C++:为什么const_cast是邪恶的?

18

我经常听到这种说法,但我并不能找到const_cast为什么被认为是有害的原因。

在以下示例中:

template <typename T>
void OscillatorToFieldTransformer<T>::setOscillator(const SysOscillatorBase<T> &src)
{
    oscillatorSrc = const_cast<SysOscillatorBase<T>*>(&src);
}

我正在使用一个引用,通过使用const修饰符,我保护了我的引用不被更改。另一方面,如果我不使用const_cast,代码将无法编译。为什么在这里使用const_cast是有害的?

对于下面的例子同样适用:

template <typename T>
void SysSystemBase<T>::addOscillator(const SysOscillatorBase<T> &src)
{
    bool alreadyThere = 0;
    for(unsigned long i = 0; i < oscillators.size(); i++)
    {
        if(&src == oscillators[i])
        {
            alreadyThere = 1;
            break;
        }
    }
    if(!alreadyThere)
    {
        oscillators.push_back(const_cast<SysOscillatorBase<T>*>(&src));
    }
}

请提供一些例子,让我看到使用const_cast是一个不好的想法/不专业的行为。

感谢您的任何努力 :)


5
const 的整个目的是防止您修改某些内容,这就是为什么您的代码会产生错误。添加 const_cast 基本上是告诉编译器闭嘴,你知道自己在做什么。这就是为什么这不是一个好主意。如果您不希望它是 const,则不要声明它为 const - Cody Gray
5
唯一可能是邪恶的东西是“程序员”。const_cast 不是邪恶的,只是在不必要的情况下不应使用。 - Rafał Rawicki
请查看这个问题 - Peter Wood
4
在达到了一定的专业水平之后,你会进一步提高专业水平,并意识到你根本不需要使用“const_cast”,实际上一开始就有更好的方法可以解决问题。 - Seth Carnegie
2
这些函数都不应该使用 const SysOscillatorBase<T> &。相反,它们应该使用 SysOscillatorBase<T> &。由于 oscillatorSrc 不是 const,程序员告诉编译器(和全世界)它保留了干涉 oscillatorSrc 内部的权利。如果你没有改变 oscillatorSrc,那么它也应该是 const,你的问题就解决了。向量也是一样的。 - Mike DeSimone
显示剩余4条评论
9个回答

37
因为你破坏了 const 的目的,这个目的是防止你修改参数。如果你去掉了某个东西的 const 属性,那么这是无意义的,会让你的代码变得臃肿,并且它允许你打破向函数使用者做出的承诺,即你不会修改参数。
此外,使用 const_cast 可能会导致未定义的行为。考虑以下代码:
SysOscillatorBase<int> src;
const SysOscillatorBase<int> src2;

...

aFieldTransformer.setOscillator(src);
aFieldTransformer.setOscillator(src2);
在第一次调用时,一切都很好。您可以强制转换一个不是真正 const 的对象并进行修改。然而,在第二个调用中,在 setOscillator 中,您正在强制转换一个真正 const 的对象的 const,如果您在其中任何地方修改该对象,将导致未定义行为,因为您正在修改一个真正是 const 的对象。由于您无法确定在声明对象时标记为 const 的对象是否真正是 const,因此除非您确定永远不会改变对象,否则永远不应使用 const_cast。如果您永远不会改变对象,那么这样做有什么意义呢?
在您的示例代码中,您正在存储对可能是 const 的对象的非-const 指针,这表明您打算更改该对象(否则为什么不只存储指向 const 的指针?)。这可能会导致未定义行为。 另外,以那种方式做还允许人们将临时值传递给您的函数:
blah.setOscillator(SysOscillatorBase<int>()); // compiles
然后你正在存储一个临时指针,当函数返回时将会失效1。如果使用非const引用就不会有这个问题。

另一方面,如果我不使用const_cast,代码就不能编译。

那就修改你的代码,不要添加转换来让它能编译通过。编译器没有为某种原因而编译它。既然你知道了原因,你可以让你的vector持有指向const的指针,而不是把一个方形洞强行变成圆形来适应你的钉子。
所以,总之,最好让你的方法接受一个非const引用,并且使用const_cast几乎从来不是一个好主意。

1实际上是在调用该函数的表达式结束时。


我相信通过将某些东西声明为const,编译器可能会在多线程代码方面做出某些假设。如果您使用const_cast并且违背了您的const承诺,那么您可能会在不同线程中得到不同的值。 - criddell
@criddell 是的,那也是一个重要的部分。 - Seth Carnegie
5
如果你确定永远都不会改变对象,那么才可以使用const_cast。而如果不需要更改对象,那使用const_cast有什么意义呢?也许你的代码需要将参数传递给某个第三方函数,但是该函数没有遵循const正确性,并且你知道它永远不会修改传递的实例。在这种情况下,如果你想使自己的代码符合const正确性并仍能将实例传递给该函数,那么可以使用const_cast。 - Kaiserludi
如果两个API都是Microsoft,一个返回常量,然后下一个需要非常量,该怎么办? - Andrew Truckle

11

使用const,我可以保护我的引用不被更改

一旦初始化,引用就不能被更改,它们始终指向同一个对象。使用const修饰的引用意味着它所引用的对象不能被更改。但是使用const_cast可以撤销这个限制,最终允许对象被更改。

另一方面,如果我不使用const_cast,则代码无法编译

这并不是什么正当理由。C++拒绝编译那些可能会导致const对象被更改的代码,因为这就是const的含义。这样的程序是错误的。使用const_cast是编译错误程序的手段,这才是问题所在。

例如,在你的程序中,看起来你有一个对象

std::vector< SysOscillatorBase<T> * > oscillators

考虑这个:

Oscillator o; // Create this object and obtain ownership
addOscillator( o ); // cannot modify o because argument is const
// ... other code ...
oscillators.back()->setFrequency( 3000 ); // woops, changed someone else's osc.

将一个对象通过const引用传递不仅意味着被调用的函数不能更改它,而且该函数也无法将其传递给其他可以更改它的人。const_cast违反了这一点。

C++的优势在于它提供了有关所有权和值语义的保证工具。当您禁用这些工具以使程序编译时,会导致错误。没有好的程序员能接受这种情况。

作为解决此特定问题的方法,看起来 vector(或任何您正在使用的容器)应存储对象的值,而不是指针。然后,addOscillator可以接受一个const引用,但存储的对象是可修改的。此外,容器拥有这些对象并确保它们被安全删除,而无需您做任何工作。


9
除了适应(旧)库的接口具有非const指针/引用但实现不修改参数以外,任何其他原因使用const_cast都是错误和危险的。其原因在于当您的接口获取对常量对象的引用或指针时,您承诺不更改该对象。其他代码可能依赖于您不修改该对象。例如,请考虑一个包含昂贵的成员和一些其他不变式的类型。考虑一个vector和预先计算的平均值,每当通过类接口添加新元素时,*average将更新,因为这样更新很便宜,如果经常请求,则无需每次从数据中重新计算它。因为向量很难复制,但可能需要读取访问,所以该类型可以提供一种廉价的访问器,返回std :: vector const&来供用户代码检查已经在容器中的值。现在,如果用户代码取消引用引用的const性并更新向量,则打破了类持有平均值的不变式,程序的行为变得不正确。除此之外,由于您没有保证传递给您的对象实际上是可修改的还是不可修改的,因此这也是危险的。考虑一个简单的函数,它获取一个C null终止字符串并将其转换为大写,足够简单:
void upper_case( char * p ) {
   while (*p) {
     *p = ::to_upper(*p);
      ++p;
   }
}

现在假设你决定改变接口以接收 const char*,并移除 const。旧版本的用户代码也可以在新版本中工作,但是一些在旧版中标记为错误的代码现在将不会在编译时检测到。考虑有人决定做像 upper_case( typeid(int).name() ) 这样愚蠢的事情。问题在于,typeid 的结果不仅仅是一个“可修改对象的常量引用”,而是一个“常量对象的引用”。编译器可以自由地将 type_info 对象存储在只读段中,并将其加载到内存的只读页面中。试图更改它将使程序崩溃。
请注意,在两种情况下,你无法从 const_cast 的上下文中知道是否保持了额外的不变量(情况1),甚至无法知道对象是否实际上是常量(情况2)。
相反,const_cast 存在的原因是为了适应旧的不支持 const 关键字的 C 语言代码。一段时间内,像 strlen 这样的函数将接收一个 char*,即使已知并记录下该函数不会修改对象。在这种情况下,使用 const_cast适应 类型,而不是改变 const-ness 是安全的。请注意,C 语言已经支持 const 很长一段时间了,因此 const_cast 的合理用途更少。

在示例中使用那个函数实际上有更多好处,可能乍一看不太明显,加一。 - Seth Carnegie
@SethCarnegie:哈哈,我错过了++p,谢谢你指出来。 - David Rodríguez - dribeas
“除了适应(旧)库之外的任何原因” - 我同意,但试着告诉那些相信梅耶尔在《Effective C++》中关于const和非const访问器的人们 :-( - Steve Jessop

3
< p > 使用 const_cast 是不好的,因为它允许您打破方法指定的协议,即“我不应修改src”。调用者期望该方法坚持这一点。


3

虽然我无法找到为什么const_cast是邪恶的原因。

当使用得当且知道自己在做什么时,它并不邪恶。(或者你真的会为所有仅由其const修饰符区别的方法复制粘贴代码吗?)

但是,const_cast的问题在于,如果您将其用于最初是const的变量,则可能会触发未定义的行为。也就是说,如果您声明了const变量,然后对其进行const_cast并尝试修改它。而未定义的行为是不好的。

您的示例恰好包含此情况:可能将const变量转换为非const。为避免该问题,请在对象列表中存储const SysOscillatorBase<T>*(const指针)或SysOscillatorBase<T>(副本),或传递引用而不是const引用。


3
这至少是有问题的。您必须区分两个常量性:
- 实例化变量的常量性 这可能导致物理上的常量性,数据被放置在只读段中。
- 引用参数/指针的常量性 这是一种逻辑上的常量性,仅由编译器强制执行。
只有当参数不是物理常量时才允许去掉const修饰符,您无法从参数中确定这一点。 此外,这表明您的代码某些部分是const-correct(即正确使用const),而另一些部分则不是。这有时是不可避免的。
在第一个示例中,您将const引用赋给了我假定为非const指针的对象。这将允许您修改原始对象,这至少需要一个const cast。举个例子:
SysOscillatorBase<int> a;
const SysOscillatorBase<int> b;

obj.setOscillator(a); // ok, a --> const a --> casting away the const
obj.setOscilaltor(b); // NOT OK: casting away the const-ness of a const instance

同样适用于您的第二个例子。

2

您违反了编码合同。将一个值标记为const意味着您可以使用该值,但永远不会更改它。const_cast会破坏这个承诺并可能导致意外的行为。

在您给出的例子中,似乎您的代码还不太对。oscillatorSrc应该是一个const指针,尽管如果您确实需要更改该值,则根本不应将其作为const传递。


1
基本上,const 保证你和编译器不会改变值。唯一应该使用它的时候是在使用已知不会更改值的 C 库函数时(在 const 不存在的情况下)。
bool compareThatShouldntChangeValue(const int* a, const int* b){
    int * c = const_cast<int*>(a);
    *c = 7;
    return a == b;
}

int main(){
   if(compareThatShouldntChangeValue(1, 7)){
      doSomething();
   }
}

0

你可能需要将容器定义为包含const对象

template <typename T> struct Foo {
    typedef std::vector<SysOscillator<T> const *>  ossilator_vector;
}

Foo::ossilator_vector<int> oscillators;


// This will compile
SysOscillator<int> const * x = new SysOscillator<int>();
oscillators.push_back(x);

// This will not
SysOscillator<int> * x = new SysOscillator<int>();
oscillators.push_back(x);

话虽如此,如果您无法控制容器的typedef,则在您的代码和库之间的接口处进行const_cast可能是可以的。


如果您无法控制容器,则方法不应该使用const参数。 - Mike DeSimone
如果您无法控制的库编写不良,很明显容器应该保持const引用但是const声明被省略了,那么为什么不确保您的代码保持const引用并在与不好的库的接口处使用const_cast进行适配。当库被修复后,则删除const_cast适配。显然,如果您错误地将物理const对象传递给库并尝试对其进行更改,则可能会崩溃 - bradgonesurfing
自从它被创建以来?至少自从他们使用operator <<进行文本输出。正确使用const是一个需要教授的概念,当然,但这并不难。它比C语言选择int作为默认函数返回类型和实现for循环更容易理解。 - Mike DeSimone

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接