修改* this而不使用const_cast的Const方法

时间:2010-08-14 16:47:49

标签: c++ const mutable const-correctness

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

#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*,并且在调用堆栈的下方,FooOwner合法地具有它使用的非const指针这一事实感到困惑修改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;
    }
};

5 个答案:

答案 0 :(得分:25)

请考虑以下事项:

int i = 3;

i是一个对象,它的类型为int。它不是cv合格的(不是constvolatile,或两者兼而有之。)

现在我们添加:

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

j是引用i的引用,k是指向i的指针。 (从现在开始,我们只需将“引用”和“指向”组合成“指向”。)

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

  

对cv限定类型的指针或引用实际上不需要指向或引用cv限定的对象,但它被视为具有;即使引用的对象是非const对象并且可以通过某些其他访问路径进行修改,也不能使用const限定的访问路径来修改对象。 [注意:类型系统支持cv-qualifiers,因此在不进行强制转换的情况下不能破坏它们(5.2.11)。 ]

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

我们现在考虑从那些中移除const

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

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

  

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

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

这与做:

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

j = 5;
*k = 5;

这是非常合法的。我们现在认为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;
};

const bar对会员的行为是什么?它允许访问它们通过称为 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函数中回收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& boo() const的返回类型中剥离const限定。

(除非有人首先用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标准。

答案 1 :(得分:6)

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对象,所以你没有做任何坏事,只是利用const正确性在C ++中工作的方式来利用一个奇怪的漏洞。

答案 2 :(得分:1)

如果不确定是否/应该/可以允许,我会非常反对它。在你想要实现的目标中,有一些机制不需要编写模糊的结构,这很可能会使其他开发人员感到困惑。

查看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成员的单线程访问。

答案 3 :(得分:0)

仅在编译时检查期间考虑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,所以我必须进行评估 进入循环时,datalength只有一次。开心辞典! 如果您尝试调试此错误,如果您使用优化进行编译(而不是在调试版本中),那么很可能只会出现这种错误,您会让自己疯狂。

信守诺言!或者在高度戒备时使用你的braincells变异!

答案 4 :(得分:-1)

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

为什么Questionable const如果您知道您将通过致电其所有者来修改它?为什么拥有的对象需要知道所有者?如果你真的需要这样做,那么mutable就是你要走的路。这就是它的用途 - 逻辑constness(与严格的bit level constness相对)。

从我的n3090草案副本中获取:

  

9.3.2此指针 [class.this]

     

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

     

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

[注意强调我的]。

在UB上:

  

7.1.6.1 cv-qualifiers

     

3 实际上不需要指向cv限定类型的指针或引用   指向或参考cv合格的   对象,但它被视为它   做;一个const限定的访问路径   不能用于修改对象   即使引用的对象是a   非const对象,可以修改   通过其他一些访问路径。 [   注意:支持cv限定符   类型系统,使他们不能   没有施放而被颠覆(5.2.11)。    - 后注]

     

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