不使用 const_cast 修改 *this 的 const 方法

2022-01-23 00:00:00 mutable constants const-correctness c++

我正在编写的程序中出现了以下模式.我希望它不是太做作,但它设法在 const 方法 Foo::Questionable() const 中改变一个 Foo 对象,而不使用任何 const_cast 或类似的东西.基本上,Foo 存储对 FooOwner 的引用,反之亦然,在 Questionable() 中,Foo 设法修改通过在其所有者上调用 mutate_foo() 将其自身置于 const 方法中.问题跟随代码.

The following pattern has arisen in a program I'm writing. I hope it's not too contrived, but it manages to mutate a Foo object in the const method Foo::Questionable() const, without use of any const_cast or similar. Basically, Foo stores a reference to FooOwner and vice versa, and in Questionable(), Foo manages to modify itself in a const method by calling mutate_foo() on its owner. Questions follow the code.

#include "stdafx.h"
#include <iostream>
using namespace std;

class FooOwner;

class Foo {
    FooOwner& owner;
    int data;

public:
    Foo(FooOwner& owner_, int data_)
        : owner(owner_),
          data(data_)
    {
    }

    void SetData(int data_)
    {
        data = data_;
    }

    int Questionable() const;       // defined after FooOwner
};

class FooOwner {
    Foo* pFoo;

public:
    FooOwner()
        : pFoo(NULL)
    {}

    void own(Foo& foo)
    {
        pFoo = &foo;
    }

    void mutate_foo()
    {
        if (pFoo != NULL)
            pFoo->SetData(0);
    }
};

int Foo::Questionable() const
{
    owner.mutate_foo();     // point of interest
    return data;
}

int main()
{
    FooOwner foo_owner;
    Foo foo(foo_owner, 0);      // foo keeps reference to foo_owner
    foo_owner.own(foo);         // foo_owner keeps pointer to foo

    cout << foo.Questionable() << endl;  // correct?

    return 0;
}

这是定义的行为吗?Foo::data 是否应该被声明为可变的?或者这是我做错事的迹象?我正在尝试实现一种延迟初始化的数据",它仅在请求时设置,并且以下代码编译良好,没有警告,所以我有点紧张我在 UB 领域.

Is this defined behavior? Should Foo::data be declared mutable? Or is this a sign I'm doing things fatally wrong? I'm trying to implement a kind of lazy-initialised 'data' which is only set when requested, and the following code compiles fine with no warnings, so I'm a little nervous I'm in UB land.

Questionable() 上的 const 仅使直接成员为 const,而不是对象指向或引用的对象.这是否使代码合法?我很困惑,在 Questionable() 中,this 的类型为 const Foo*,而在调用堆栈的下方,FooOwner 合法地有一个非常量指针,用于修改 Foo.这是否意味着 Foo 对象可以修改或不可修改?

the const on Questionable() only makes immediate members const, and not the objects pointed to or referenced by the object. Does this make the code legal? I'm confused between the fact that in Questionable(), this has the type const Foo*, and further down the call stack, FooOwner legitimately has a non-const pointer it uses to modify Foo. Does this mean the Foo object can be modified or not?

编辑 2:也许是一个更简单的例子:

Edit 2: perhaps an even simpler example:

class X {
    X* nonconst_this;   // Only turns in to X* const in a const method!
    int data;

public:
    X()
        : nonconst_this(this),
          data(0)
    {
    }

    int GetData() const
    {
        nonconst_this->data = 5;    // legal??
        return data;
    }
};

推荐答案

考虑以下几点:

int i = 3;

i 是一个对象,它的类型是 int.它不是 cv 限定的(不是 constvolatile,或两者兼有.)

i is an object, and it has the type int. It is not cv-qualified (is not const or volatile, or both.)

现在我们添加:

const int& j = i;
const int* k = &i;

j是引用i的引用,k是指向i的指针.(从现在开始,我们将引用"和指向"简单地组合为指向".)

j is a reference which refers to i, and k is a pointer which points to i. (From now on, we simply combine "refer to" and "points to" to just "points to".)

此时,我们有两个 cv 限定变量,jk,它们指向一个非 cv 限定对象.§7.1. 5.1/3 中提到了这一点:

At this point, we have two cv-qualified variables, j and k, that point to a non-cv-qualified object. This is mentioned in §7.1.?5.1/3:

指向 cv 限定类型的指针或引用不需要实际指向或引用 cv 限定对象,但它被视为好像确实如此;即使引用的对象是非常量对象并且可以通过其他访问路径进行修改,也不能使用 const 限定的访问路径来修改对象.[注意:类型系统支持 cv 限定符,因此如果不进行强制转换(5.2.11),它们就不能被颠覆.]

A pointer or reference to a cv-qualified type need not actually point or refer to a cv-qualified object, but it is treated as if it does; a const-qualified access path cannot be used to modify an object even if the object referenced is a non-const object and can be modified through some other access path. [Note: cv-qualifiers are supported by the type system so that they cannot be subverted without casting (5.2.11). ]

这意味着编译器必须尊重 jk 是 cv 限定的,即使它们指向非 cv 限定的对象.(所以 j = 5*k = 5 是非法的,尽管 i = 5 是合法的.)

What this means is that a compiler must respect that j and k are cv-qualified, even though they point to a non-cv-qualified object. (So j = 5 and *k = 5 are illegal, even though i = 5 is legal.)

我们现在考虑从这些中删除 const:

We now consider removing the const from those:

const_cast<int&>(j) = 5;
*const_cast<int*>(k) = 5;

这是合法的(§参阅 5.2.11),但它是未定义的行为吗?否.参见§7.1. 5.1/4:

This is legal (§refer to 5.2.11), but is it undefined behavior? No. See §7.1.?5.1/4:

除了可以修改任何声明为可变的类成员(7.1.1)外,任何在 const 对象的生命周期(3.8)期间修改它的尝试都会导致未定义的行为.强调我的.

Except that any class member declared mutable (7.1.1) can be modified, any attempt to modify a const object during its lifetime (3.8) results in undefined behavior. Emphasis mine.

记住 i 是 not const 并且 jk 都是指向 i.我们所做的只是告诉类型系统从类型中删除 const 限定符,以便我们可以修改指向的对象,然后通过这些变量修改 i.

Remember that i is not const and that j and k both point to i. All we've done is tell the type system to remove the const-qualifier from the type so we can modify the pointed to object, and then modified i through those variables.

这和做的完全一样:

int& j = i; // removed const with const_cast...
int* k = &i; // ..trivially legal code

j = 5;
*k = 5;

这是非常合法的.我们现在认为 i 是这样的:

And this is trivially legal. We now consider that i was this instead:

const int i = 3;

现在我们的代码呢?

const_cast<int&>(j) = 5;
*const_cast<int*>(k) = 5;

它现在导致未定义的行为,因为i 是一个const 限定的对象.我们告诉类型系统删除 const,以便我们可以修改指向的对象,然后修改一个 const 限定的对象.如上所述,这是未定义的.

It now leads to undefined behavior, because i is a const-qualified object. We told the type system to remove const so we can modify the pointed to object, and then modified a const-qualified object. This is undefined, as quoted above.

同样,更明显的是:

int& j = i; // removed const with const_cast...
int* k = &i; // ...but this is not legal!

j = 5;
*k = 5;

请注意,只需这样做:

const_cast<int&>(j);
*const_cast<int*>(k);

完全合法且已定义,因为没有修改 const 限定的对象;我们只是在搞乱类型系统.

Is perfectly legal and defined, as no const-qualified objects are being modified; we're just messing with the type-system.

现在考虑:

struct foo
{
    foo() :
    me(this), self(*this), i(3)
    {}

    void bar() const
    {
        me->i = 5;
        self.i = 5;
    }

    foo* me;
    foo& self;
    int i;
};

bar 上的const 对成员有什么作用?它使对它们的访问通过称为 cv-qualified 访问路径的方式进行.(它通过将 this 的类型从 T* const 更改为 cv T const*,其中 cv是函数上的 cv 限定符.)

What does const on bar do to the members? It makes access to them go through something called a cv-qualified access path. (It does this by changing the type of this from T* const to cv T const*, where cv is the cv-qualifiers on the function.)

那么bar执行过程中的成员类型有哪些呢?它们是:

So what are the members types during the execution of bar? They are:

// const-pointer-to-non-const, where the pointer points cannot be changed
foo* const me;

// foo& const is ill-formed, cv-qualifiers do nothing to reference types
foo& self; 

// same as const int
int const i; 

当然,类型是无关紧要的,因为重要的是 指向 对象的 const 限定,而不是指针.(如果上面的 kconst int* const,则后面的 const 无关紧要.)我们现在考虑:

Of course, the types are irrelevant, as the important thing is the const-qualification of the pointed to objects, not the pointers. (Had k above been const int* const, the latter const is irrelevant.) We now consider:

int main()
{
    foo f;
    f.bar(); // UB?
}

bar 中,meself 都指向一个非 const foo,所以就像 withint i 上面我们有明确定义的行为.如果我们有:

Within bar, both me and self point to a non-const foo, so just like with int i above we have well-defined behavior. Had we had:

const foo f;
f.bar(); // UB!

我们应该有 UB,就像 const int 一样,因为我们将修改一个 const 限定的对象.

We would have had UB, just like with const int, because we would be modifying a const-qualified object.

在您的问题中,您没有 const 限定的对象,因此您没有未定义的行为.

In your question, you have no const-qualified objects, so you have no undefined behavior.

为了增加对权威的吸引力,请考虑 Scott Meyers 的 const_cast 技巧,用于在非 const 函数中回收 const 限定函数:

And just to add an appeal to authority, consider the const_cast trick by Scott Meyers, used to recycle a const-qualified function in a non-const function:

struct foo
{
    const int& bar() const
    {
        int* result = /* complicated process to get the resulting int */
        return *result; 
    }

    int& bar()
    {
        // we wouldn't like to copy-paste a complicated process, what can we do?
    }

};

他建议:

int& bar(void)
{
    const foo& self = *this; // add const
    const int& result = self.bar(); // call const version
    return const_cast<int&>(result); // take off const
}

或者它通常是怎么写的:

Or how it's usually written:

int& bar(void)
{
    return const_cast<int&>( // (3) remove const from result
            static_cast<const foo&>(*this) // (1) add const to this
            .bar() // (2) call const version
            ); 
}

请注意,这又是完全合法且定义明确的.具体来说,因为这个函数必须在非 const 限定的 foo 上调用,所以我们从 int& 的返回类型中剥离 const 限定是完全安全的.boo() 常量.

Note this is, again, perfectly legal and well-defined. Specifically, because this function must be called on a non-const-qualified foo, we are perfectly safe in stripping the const-qualification from the return type of int& boo() const.

(除非有人一开始就用 const_cast + 调用射击自己.)

(Unless someone shoots themselves with a const_cast + call in the first place.)

总结一下:

struct foo
{
    foo(void) :
    i(),
    self(*this), me(this),
    self_2(*this), me_2(this)
    {}

    const int& bar() const
    {
        return i; // always well-formed, always defined
    }

    int& bar() const
    {
        // always well-formed, always well-defined
        return const_cast<int&>(
                static_cast<const foo&>(*this).
                bar()
                );
    }

    void baz() const
    {
        // always ill-formed, i is a const int in baz
        i = 5; 

        // always ill-formed, me is a foo* const in baz
        me = 0;

        // always ill-formed, me_2 is a const foo* const in baz
        me_2 = 0; 

        // always well-formed, defined if the foo pointed to is non-const
        self.i = 5;
        me->i = 5; 

        // always ill-formed, type points to a const (though the object it 
        // points to may or may not necessarily be const-qualified)
        self_2.i = 5; 
        me_2->i = 5; 

        // always well-formed, always defined, nothing being modified
        // (note: if the result/member was not an int and was a user-defined 
        // type, if it had its copy-constructor and/or operator= parameter 
        // as T& instead of const T&, like auto_ptr for example, this would 
        // be defined if the foo self_2/me_2 points to was non-const
        int r = const_cast<foo&>(self_2).i;
        r = const_cast<foo* const>(me_2)->i;

        // always well-formed, always defined, nothing being modified.
        // (same idea behind the non-const bar, only const qualifications
        // are being changed, not any objects.)
        const_cast<foo&>(self_2);
        const_cast<foo* const>(me_2);

        // always well-formed, defined if the foo pointed to is non-const
        // (note, equivalent to using self and me)
        const_cast<foo&>(self_2).i = 5;
        const_cast<foo* const>(me_2)->i = 5;

        // always well-formed, defined if the foo pointed to is non-const
        const_cast<foo&>(*this).i = 5;
        const_cast<foo* const>(this)->i = 5;
    }

    int i;

    foo& self;
    foo* me;
    const foo& self_2;
    const foo* me_2;
};

int main()
{
    int i = 0;
    {
        // always well-formed, always defined
        int& x = i;
        int* y = &i;
        const int& z = i;
        const int* w = &i;

        // always well-formed, always defined
        // (note, same as using x and y)
        const_cast<int&>(z) = 5;
        const_cast<int*>(w) = 5;
    }

    const int j = 0;
    {
        // never well-formed, strips cv-qualifications without a cast
        int& x = j;
        int* y = &j;

        // always well-formed, always defined
        const int& z = i;
        const int* w = &i;

        // always well-formed, never defined
        // (note, same as using x and y, but those were ill-formed)
        const_cast<int&>(z) = 5;
        const_cast<int*>(w) = 5;
    }

    foo x;
    x.bar(); // calls non-const, well-formed, always defined
    x.bar() = 5; // calls non-const, which calls const, removes const from
                 // result, and modifies which is defined because the object
                 // pointed to by the returned reference is non-const,
                 // because x is non-const.

    x.baz(); // well-formed, always defined

    const foo y;
    y.bar(); // calls const, well-formed, always defined
    const_cast<foo&>(y).bar(); // calls non-const, well-formed, 
                               // always defined (nothing being modified)
    const_cast<foo&>(y).bar() = 5; // calls non-const, which calls const,
                                   // removes const from result, and
                                   // modifies which is undefined because 
                                   // the object pointed to by the returned
                                   // reference is const, because y is const.

    y.baz(); // well-formed, always undefined
}

我参考的是ISO C++03标准.

相关文章