9

我正在编写的程序中出现了以下模式。我希望它不是太做作,但它设法Foo在 const 方法中改变一个对象Foo::Questionable() const,而不使用任何 const_cast 或类似的东西。基本上,Foo存储对的引用,FooOwner反之亦然,并且 in Questionable(),通过调用其所有者Foo来设法在 const 方法中修改自身。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 领域。

编辑:conston Questionable() 仅使直接成员变为常量,而不是对象指向或引用的对象。这是否使代码合法?我对以下事实感到困惑,即 inQuestionable()具有this类型const Foo*,并且在调用堆栈的更下方,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;
    }
};
4

5 回答 5

27

考虑以下:

int i = 3;

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

现在我们添加:

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

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

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

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

这意味着编译器必须尊重这一点j并且k是 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) 期间修改它的尝试都会导致未定义的行为强调我的。

请记住 that iis not const和 thatjk两者都指向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-qualified access path 的方式进行。(它通过将thisfrom的类型更改为T* const来做到这一点,函数上的 cv 限定符在cv T const*哪里。)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 限定,而不是指针。(k以上是const int* const,后者const无关紧要。)我们现在考虑:

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

barme和都self指向一个非常量foo,所以就像int i上面一样,我们有明确定义的行为。如果我们有:

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

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

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


只是为了增加对权威的吸引力,请考虑const_castScott Meyers 的技巧,用于在非常量函数中回收 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-qualified 上调用foo,所以我们从 的返回类型中剥离 const-qualification 是完全安全的int& boo() 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 标准。

于 2010-08-14T22:16:31.760 回答
6

IMO,您在技术上没有做错任何事情。如果成员是指针,可能会更容易理解。

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

const使指针const,而不是pointee

考虑以下之间的区别:

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

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

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

于 2010-08-14T19:55:29.777 回答
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

于 2010-08-14T22:34:21.617 回答
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
}

在这种情况下,编译器可能会决定,嗯,foo.datalength 是 const,并且循环内的代码承诺不会更改 foo,所以当我进入循环时我只需要计算一次 datalength。伊皮!如果你尝试调试这个错误,它很可能只有在你使用优化编译时才会出现(而不是在调试版本中),你会让自己发疯。

信守诺言!或者使用 mutable 让您的脑细胞处于高度戒备状态!

于 2010-08-14T16:58:16.317 回答
-1

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

Questionable const如果您知道将通过调用其所有者来修改它,为什么会这样呢?为什么拥有的对象需要了解所有者?如果您真的需要这样做,那么mutable就是要走的路。这就是它的用途——逻辑常量(与严格的位级常量相反)。

从我的 n3090 草案副本中:

9.3.2 this指针[class.this]

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

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

[注意强调我的]。

在 UB 上:

7.1.6.1 cv 限定符

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

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

于 2010-08-14T16:57:29.043 回答