以下模式出现在我正在编写的程序中。我希望它不是太做作,但它设法在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;
}
};
答案 0 :(得分:25)
请考虑以下事项:
int i = 3;
i
是一个对象,它的类型为int
。它不是cv合格的(不是const
或volatile
,或两者兼而有之。)
现在我们添加:
const int& j = i;
const int* k = &i;
j
是引用i
的引用,k
是指向i
的指针。 (从现在开始,我们只需将“引用”和“指向”组合成“指向”。)
此时,我们有两个cv限定变量j
和k
,它们指向一个非cv限定的对象。这在§7.1.5.1/ 3中提到:
对cv限定类型的指针或引用实际上不需要指向或引用cv限定的对象,但它被视为具有;即使引用的对象是非const对象并且可以通过某些其他访问路径进行修改,也不能使用const限定的访问路径来修改对象。 [注意:类型系统支持cv-qualifiers,因此在不进行强制转换的情况下不能破坏它们(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)都会导致未定义的行为。 强调我的。
请注意,i
不 const
且j
和k
都指向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限定,而不是指针。 (如果上面k
为const int* const
,后者const
则无关紧要。)我们现在考虑:
int main()
{
foo f;
f.bar(); // UB?
}
在bar
内,me
和self
都指向非常量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)
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) 导致未定义的行为。