代码之家  ›  专栏  ›  技术社区  ›  AshleysBrain

在不使用const-cast的情况下修改*this的const方法

  •  9
  • AshleysBrain  · 技术社区  · 14 年前

    以下模式出现在我正在编写的程序中。我希望它不是太做作,但它设法变异了 Foo const方法中的对象 Foo::Questionable() const 不使用任何const ou cast或类似工具。基本上, 存储对的引用 FooOwner 反之亦然,并且 Questionable() , 通过调用 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-land。

    编辑: const 在Questible()上,只使直接成员成为常量,而不使对象指向或引用的对象成为常量。这是否使代码合法?我很困惑的是 问题() , this 有类型 const 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 回复  |  直到 13 年前
        1
  •  25
  •   Lightness Races in Orbit    13 年前

    考虑以下事项:

    int i = 3;
    

    i 是一个对象,它具有 int . 不合格(不合格) const volatile ,或者两者兼而有之。

    现在我们添加:

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

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

    在这一点上,我们有两个Cv限定变量, J K ,指向一个非CV限定对象。这在7.1.5.1/3节中提到:

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

    这意味着编译器必须尊重 J K 是合格的简历,即使他们指向一个非合格的简历对象。(所以) j = 5 *k = 5 是非法的,即使 i = 5 是合法的。)

    我们现在考虑移除 康斯特 从那些:

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

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

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

    记住 康斯特 J K 都指向 . 我们所做的就是告诉类型系统从类型中移除const限定符,这样我们可以修改指向对象,然后修改 通过这些变量。

    这与执行以下操作完全相同:

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

    这是非常合法的。我们现在考虑一下 而是这样:

    const int i = 3;
    

    我们现在的代码是什么?

    const-cast<int>(j)=5;
    *常量cast<int*>(k)=5;
    

    现在导致 未定义的行为 ,因为 是常量限定对象。我们告诉类型系统删除 康斯特 所以我们可以修改指向对象, 然后修改一个常量限定对象 . 如上文所述,这是未定义的。

    同样,更明显的是:

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

    这是完全合法和定义的,因为没有修改常量限定对象;我们只是在处理类型系统。


    现在考虑:

    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 对会员做什么?它使访问它们的权限通过一个称为 cv限定访问路径 . (它通过改变 this T* const cv T const* 在哪里 cv 是函数上的cv限定符。)

    那么在执行 酒吧 ?他们是:

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

    当然,类型是不相关的,因为重要的是 指向 对象,而不是指针。有 K 以上已 const int* const 后者 康斯特 我们现在考虑:

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

    酒吧 ,两者 me self 指向非常量 foo 就像和 int i 上面我们有明确的行为。我们曾经拥有过:

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

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

    在您的问题中,您没有常量限定对象,因此没有未定义的行为。


    为了增加对权威的吸引力,考虑一下 const_cast Scott Meyers的技巧,用于在非常量函数中循环常量限定函数:

    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限定条件。 int& boo() const .

    (除非有人用 康斯特卡斯特 +首先打电话。)


    总结:

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

        2
  •  6
  •   UncleBens    14 年前

    在我看来,你在技术上没有做错什么。可能更容易理解成员是否是指针。

    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对象,所以你没有做任何坏事,只是在C++中使用了一个奇怪的漏洞。

        3
  •  1
  •   David Rodríguez - dribeas    14 年前

    如果不考虑是否允许,我会非常反对。语言中有一些机制可以实现您想要实现的目标,而不需要编写晦涩的结构,这些结构很可能会让其他开发人员感到困惑。

    窥视 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_;
    };
    

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

    解决方案是缓存结果。为此我们可以把结果交给全班。当请求结果时,如果我们已经计算了它,我们只需要检索它,而如果我们没有这个值,我们必须计算它。当输入更改时,我们会使缓存失效。这是当 易变的 关键词很有用。它告诉编译器成员不是可感知状态的一部分,因此可以在常量方法中对其进行修改:

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

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

    这个 易变的 在其他地方需要关键字,例如在多线程应用程序中,类中的互斥体通常标记为 易变的 . 锁定和解锁互斥体正在改变互斥体的操作:它的状态明显在改变。现在,在不同线程之间共享的对象中的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 成员。

        4
  •  0
  •   AndreasT    14 年前

    这个 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,所以我必须评估 数据长度只有一次,当我进入循环时。雅皮士! 如果您尝试调试此错误,那么只有使用优化(而不是在调试版本中)进行编译时才会出现这种错误,这会使您自己疯狂。

    信守承诺!或者在你的脑细胞处于高度戒备状态时使用可变的!

        5
  •  -1
  •   dirkgently    14 年前

    已达到循环依赖项。见 FAQ 39.11 是的,修改 const 即使您绕过了编译器,数据也是ub。此外,如果不遵守承诺,则会严重削弱编译器的优化能力(阅读:违反 康斯特 )

    为什么是 Questionable 康斯特 如果您知道您将通过调用其所有者来修改它?为什么拥有的对象需要了解所有者?如果你真的需要那么做 mutable 是前进的道路。这就是它的目的——逻辑结构(与严格的位级结构相反)。

    从我的草案N3090副本:

    9.3.2本指针 [班]

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

    在const成员函数中,通过const访问来访问为其调用函数的对象。 因此,常量成员函数不应修改对象及其非静态数据成员。

    [注意强调我的]。

    关于UB:

    7.1.6.1 CV限定符

    指向cv限定类型的指针或引用实际上不需要 点或参考合格的简历 对象,但它被视为 是;常量限定的访问路径 不能用于修改对象 即使引用的对象是 非常量对象,可以修改 通过其他访问路径。[ 注意:cv限定符支持 类型系统,以便它们不能 未铸造而颠覆(5.2.11)。 结束注释

    除了任何一个班 声明为可变的成员(7.1.1)可以是 修改,任何尝试修改 常量对象在其生存期内(3.8) 导致未定义的行为。