无需 const_cast 即可修改 *this 的 Const 方法

发布于 2024-09-14 01:01:35 字数 2054 浏览 8 评论 0原文

我正在编写的程序中出现了以下模式。我希望它不是太做作,但它设法在 const 方法 Foo::Questionable() const 中改变 <​​code>Foo 对象,而不使用任何 const_cast 或类似的方法。基本上,Foo 存储对 FooOwner 的引用,反之亦然,并且在 Questionable() 中,Foo 设法修改通过对其所有者调用 mutate_foo() 来将其自身放入 const 方法中。问题遵循代码。

#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 土地上。

编辑:Questionable() 上的 const 仅使直接成员为 const,而不是对象指向或引用的对象。这使代码合法吗?我对以下事实感到困惑:在 Questionable() 中,this 的类型为 const Foo*,而在调用堆栈的更深处,< code>FooOwner 合法地拥有一个用于修改 Foo 的非常量指针。这是否意味着 Foo 对象可以修改?

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

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;
    }
};

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;
}

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.

Edit: 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?

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;
    }
};

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

眉黛浅 2024-09-21 01:01:35

考虑以下情况:

int i = 3;

i 是一个对象,并且其类型为 int。它不是 cv 限定的(不是 constvolatile,或两者。)

现在我们添加:

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

j 是一个引用 < code>i,k是指向i的指针。 (从现在开始,我们只需将“refer to”和“points to”合并为“points to”。)

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

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

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

我们现在考虑删除 >const from those:

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

这是合法的(§参考5.2.11),但它是未定义的行为吗? 否。请参阅第 7.1 节。 5.1/4:

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

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

这与这样做完全相同:

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

j = 5;
*k = 5;

And this is trivially legal.我们现在认为 i 是这样的:

const int i = 3;

现在我们的代码是什么?

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

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

再次,更明显的是:

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 限定的对象被修改;我们只是在搞乱类型系统。


现在考虑一下:

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 限定的访问路径的方式进行。 (它通过将 this 的类型从 T* const 更改为 cv T const* 来实现此目的,其中 cv是函数上的 cv 限定符。)

那么 bar 执行期间的成员类型是什么?它们是:

// 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 是无关紧要的。)我们现在考虑:

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

bar 内>,meself 都指向一个非常量 foo,所以就像上面的 int i 一样有明确的行为。如果我们有:

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

我们就会有 UB,就像 const int 一样,因为我们将修改 const 限定的对象。

在你的问题中,你没有 const 限定的对象,所以你没有未定义的行为。


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

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
}

或者通常如何编写:

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() 常量

(除非有人一开始就用 const_cast + 调用自杀。)


总结一下:

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 标准。

Consider the following:

int i = 3;

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

Now we add:

const int& j = i;
const int* 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".)

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:

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). ]

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.)

We now consider removing the const from those:

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

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

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.

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.

This is exactly the same as doing:

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

j = 5;
*k = 5;

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

const int i = 3;

What of our code now?

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

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.

Again, more apparent as:

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

j = 5;
*k = 5;

Note that simply doing this:

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

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


Now consider:

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

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

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

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.)

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; 

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?
}

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!

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

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


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?
    }

};

He suggests:

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
            ); 
}

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.

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


To summarize:

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
}

I refer to the ISO C++03 standard.

甜`诱少女 2024-09-21 01:01:35

IMO,你没有做任何技术上错误的事情。如果该成员是一个指针,也许会更容易理解。

class X
{
    Y* m_ptr;
    void foo() const {
        m_ptr = NULL; //illegal
        *m_ptr = 42; //legal
    }
};

const 使指针成为const,而不是被指向者

考虑以下之间的区别:

const X* ptr;
X* const ptr;  //this is what happens in const member functions

对于引用,由于它们无论如何都无法重新定位,因此方法上的 const 关键字对引用成员没有任何影响。

在你的例子中,我没有看到任何 const 对象,所以你没有做任何坏事,只是利用了 C++ 中 const 正确性工作方式中的一个奇怪的漏洞。

IMO, you are not doing anything technically wrong. May-be it would be simpler to understand if the member was a pointer.

class X
{
    Y* m_ptr;
    void foo() const {
        m_ptr = NULL; //illegal
        *m_ptr = 42; //legal
    }
};

const makes the pointer const, not the pointee.

Consider the difference between:

const X* ptr;
X* const ptr;  //this is what happens in const member functions

As to references, since they can't be reseated anyway, the const keyword on the method has no effect whatsoever on reference members.

In your example, I don't see any const objects, so you are not doing anything bad, just exploiting a strange loophole in the way const correctness works in C++.

尾戒 2024-09-21 01:01:35

在没有真正了解它是否/应该/可以被允许的情况下,我强烈建议反对它。语言中有一些机制可以实现您想要实现的目标,不需要编写很可能会让其他开发人员感到困惑的晦涩构造。

查看 mutable 关键字。该关键字可用于声明可在 const 成员方法中修改的成员,因为它们不会影响类的可感知状态。考虑使用一组参数初始化并执行可能并不总是需要的复杂昂贵计算的类:

class ComplexProcessor
{
public:
   void setInputs( int a, int b );
   int getValue() const;
private:
   int complexCalculation( int a, int b );
   int result;
};

一种可能的实现是将结果值添加为成员并为每个组计算它:

void ComplexProcessor::setInputs( int a, int b ) {
   result = complexCalculation( a, b );
}

但这意味着该值是在所有套装,无论是否需要。如果您将对象视为黑匣子,则该接口仅定义一个用于设置参数的方法和一个用于检索计算值的方法。执行计算的瞬间并不真正影响对象的感知状态——只要 getter 返回的值是正确的。因此,我们可以修改该类来存储输入(而不是输出),并仅在需要时计算结果:

class ComplexProcessor2 {
public:
   void setInputs( int a, int b ) {
      a_ = a; b_ = b;
   }
   int getValue() const {
      return complexCalculation( a_, b_ );
   }
private:
   int complexCalculation( int a, int b );
   int a_,b_;
};

从语义上讲,第二类和第一类是等效的,但现在我们避免了在值不是时执行复杂的计算。需要,因此如果仅在某些情况下请求该值是一个优势。但同时,如果对同一对象多次请求该值也是一个缺点:即使输入没有更改,每次也会执行复杂的计算。

解决方案是缓存结果。为此我们可以将结果传达给班级。当请求结果时,如果我们已经计算了它,我们只需要检索它,而如果我们没有该值,我们必须计算它。当输入发生变化时,我们会使缓存失效。这就是 mutable 关键字派上用场的时候。它告诉编译器该成员不是可感知状态的一部分,因此可以在常量方法中对其进行修改:

class ComplexProcessor3 {
public:
   ComplexProcessor3() : cached_(false) {}
   void setInputs( int a, int b ) {
      a_ = a; b_ = b;
      cached_ = false;
   }
   int getValue() const {
      if ( !cached_ ) {
         result_ = complexCalculation( a_, b_ );
         cached_ = true;
      }
      return result_;
   }
private:
   int complexCalculation( int a, int b );
   int a_,b_;
   // This are not part of the perceivable state:
   mutable int result_;
   mutable bool cached_;
};

第三个实现在语义上等同于前两个版本,但如果结果已经是则避免重新计算值已知并缓存。

在其他地方需要 mutable 关键字,例如在多线程应用程序中,类中的互斥体通常被标记为 mutable。锁定和解锁互斥体是互斥体的变异操作:其状态明显发生变化。现在,在不同线程之间共享的对象中的 getter 方法不会修改感知的状态,但如果操作必须是线程安全的,则必须获取和释放锁:

template <typename T>
class SharedValue {
public:
   void set( T v ) {
      scoped_lock lock(mutex_);
      value = v;
   }
   T get() const {
      scoped_lock lock(mutex_);
      return value;
   }
private:
   T value;
   mutable mutex mutex_;
};

getter 操作在语义上是恒定的,即使它需要修改互斥体以确保单线程访问 value 成员。

Without actually getting to whether it is/should/could be allowed, I would greatly advice against it. There are mechanisms in the language for what you want to achieve that don't require writing obscure constructs that will most probably confuse other developers.

Look into the mutable keyword. That keyword can be used to declare members that can be modified within const member methods as they do not affect the perceivable state of the class. Consider class that gets initialized with a set of parameters and performs a complex expensive calculation that may not be needed always:

class ComplexProcessor
{
public:
   void setInputs( int a, int b );
   int getValue() const;
private:
   int complexCalculation( int a, int b );
   int result;
};

A possible implementation is adding the result value as a member and calculating it for each set:

void ComplexProcessor::setInputs( int a, int b ) {
   result = complexCalculation( a, b );
}

But this means that the value is calculated in all sets, whether it is needed or not. If you think on the object as a black box, the interface just defines a method to set the parameters and a method to retrieve the calculated value. The instant when the calculation is performed does not really affect the perceived state of the object --as far as the value returned by the getter is correct. So we can modify the class to store the inputs (instead of the outputs) and calculate the result only when needed:

class ComplexProcessor2 {
public:
   void setInputs( int a, int b ) {
      a_ = a; b_ = b;
   }
   int getValue() const {
      return complexCalculation( a_, b_ );
   }
private:
   int complexCalculation( int a, int b );
   int a_,b_;
};

Semantically the second class and the first class are equivalent, but now we have avoided to perform the complex calculation if the value is not needed, so it is an advantage if the value is only requested in some cases. But at the same time it is a disadvantage if the value is requested many times for the same object: each time the complex calculation will be performed even if the inputs have not changed.

The solution is caching the result. For that we can the result to the class. When the result is requested, if we have already calculated it, we only need to retrieve it, while if we do not have the value we must calculate it. When the inputs change we invalidate the cache. This is when the mutable keyword comes in handy. It tells the compiler that the member is not part of the perceivable state and as such it can be modified within a constant method:

class ComplexProcessor3 {
public:
   ComplexProcessor3() : cached_(false) {}
   void setInputs( int a, int b ) {
      a_ = a; b_ = b;
      cached_ = false;
   }
   int getValue() const {
      if ( !cached_ ) {
         result_ = complexCalculation( a_, b_ );
         cached_ = true;
      }
      return result_;
   }
private:
   int complexCalculation( int a, int b );
   int a_,b_;
   // This are not part of the perceivable state:
   mutable int result_;
   mutable bool cached_;
};

The third implementation is semantically equivalent to the two previous versions, but avoid having to recalculate the value if the result is already known --and cached.

The mutable keyword is needed in other places, like in multithreaded applications the mutex in classes are often marked as mutable. Locking and unlocking a mutex are mutating operations for the mutex: its state is clearly changing. Now, a getter method in an object that is shared among different threads does not modify the perceived state but must acquire and release the lock if the operation has to be thread safe:

template <typename T>
class SharedValue {
public:
   void set( T v ) {
      scoped_lock lock(mutex_);
      value = v;
   }
   T get() const {
      scoped_lock lock(mutex_);
      return value;
   }
private:
   T value;
   mutable mutex mutex_;
};

The getter operation is semantically constant, even if it needs to modify the mutex to ensure single threaded access to the value member.

牛↙奶布丁 2024-09-21 01:01:35

const 关键字仅在编译时检查期间被考虑。 C++ 没有提供任何工具来保护您的类免受任何内存访问,而这正是您对指针/引用所做的事情。编译器和运行时都无法知道您的指针是否指向您在某处声明为 const 的实例。

编辑:

简短的示例(可能无法编译):

// lets say foo has a member const int Foo::datalength() const {...}
// and a read only acces method const char data(int idx) const {...}

for (int i; i < foo.datalength(); ++i)
{
     foo.questionable();  // this will most likely mess up foo.datalength !!
     std::cout << foo.data(i); // HERE BE DRAGONS
}

在这种情况下,编译器可能会决定,ey,foo.datalength 是 const,
并且循环内的代码承诺不会更改 foo,所以我必须评估
当我进入循环时,数据长度仅一次。雅皮士!
如果你尝试调试这个错误,这个错误很可能只有在你使用优化编译(而不是在调试版本中)时才会出现,你会让自己发疯的。

信守承诺!或者使用 mutable 让你的脑细胞保持高度警惕!

The const keyword is only considered during compile time checks. C++ provides no facilities to protect your class against any memory access, which is what you are doing with your pointer/reference. Neither the compiler nor the runtime can know if your pointer points to an instance that you declared const somewhere.

EDIT:

Short example (might not compile):

// lets say foo has a member const int Foo::datalength() const {...}
// and a read only acces method const char data(int idx) const {...}

for (int i; i < foo.datalength(); ++i)
{
     foo.questionable();  // this will most likely mess up foo.datalength !!
     std::cout << foo.data(i); // HERE BE DRAGONS
}

In this case, the compiler might decide, ey, foo.datalength is const,
and the code inside the loop promised not to change foo, so I have to evaluate
datalength only once when I enter the loop. Yippie!
And if you try to debug this error, which will most likely only turn up if you compile with optimizations (not in the debug builds) you will drive yourself crazy.

Keep the promises! Or use mutable with your braincells on high alert!

沒落の蓅哖 2024-09-21 01:01:35

您已经达到了循​​环依赖关系。请参阅常见问题解答 39.11 是的,即使您已经绕过了编译器,修改 const 数据也是 UB 的。此外,如果您不遵守承诺(请阅读:违反const),您将严重损害编译器的优化能力。

如果您知道将通过调用其所有者来修改它,为什么有问题 const?为什么被拥有的对象需要知道所有者?如果您确实需要这样做,那么 mutable 就是正确的选择。这就是它的作用——逻辑常量(与严格的位级常量相对)。

从我的 n3090 草案副本中:

9.3.2 this 指针 [class.this]

1 在非静态 (9.3) 成员函数体内,关键字 this 是一个右值,一个右值表达式,其
value 是调用该函数的对象的地址。成员函数中 this 的类型
X 类的值为 X*。 如果成员函数声明为const,则this的类型为const X*,如果成员函数声明为const,则this的类型为const X*
函数被声明为 volatile,则 this 的类型为 volatile X*,并且如果声明了成员函数
const 易失性,其类型为 const 易失性 X*。

2 在 const 成员函数中,调用该函数的对象是通过 const 访问来访问的
小路;因此,const 成员函数不得修改对象及其非静态数据成员。

[注意强调我的]。

关于布法罗:

7.1.6.1 cv 限定符

3 指向 cv 限定类型的指针或引用实际上不需要
指出或提及合格的简历
对象,但它被视为好像
做; const 限定的访问路径
不能用于修改对象
即使引用的对象是
非常量对象,可以修改
通过其他一些访问路径。 [
注意:cv 限定符受以下支持
类型系统,以便它们不能
无需铸造即可颠覆(5.2.11)。
——尾注]

4 除了任何类
声明可变的成员(7.1.1)可以是
已修改,任何修改的尝试
const 对象在其生命周期内 (3.8)
导致未定义的行为。

You have reached circular dependencies. See FAQ 39.11 And yes, modifying const data is UB even if you have circumvented the compiler. Also, you are severely impairing the compiler's capacity to optimize if you don't keep your promises (read: violate const).

Why is Questionable const if you know that you will modify it via a call to its owner? Why does the owned object need to know about the owner? If you really really need to do that then mutable is the way to go. That is what it is there for -- logical constness (as opposed to strict bit level constness).

From my copy of the draft n3090:

9.3.2 The this pointer [class.this]

1 In the body of a non-static (9.3) member function, the keyword this is an rvalue a prvalue expression whose
value is the address of the object for which the function is called. The type of this in a member function
of a class X is X*. If the member function is declared const, the type of this is const X*, if the member
function is declared volatile, the type of this is volatile X*, and if the member function is declared
const volatile, the type of this is const volatile X*.

2 In a const member function, the object for which the function is called is accessed through a const access
path; therefore, a const member function shall not modify the object and its non-static data members.

[Note emphasis mine].

On UB:

7.1.6.1 The cv-qualifiers

3 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).
—end note ]

4 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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文