当前位置:  开发笔记 > 编程语言 > 正文

如何防止在非const对象上意外调用变异函数?

如何解决《如何防止在非const对象上意外调用变异函数?》经验,为你挑选了5个好方法。

假设我们有一个类型为myType的Object obj,我们希望将它传递给函数Foo,它返回一些关于obj的有价值的信息.函数Bar是声明obj的地方,并且从中调用Foo,如下所示:

void Bar ()
{

myType obj; //default constructor

string valuableInfo = Foo(obj); 

//do other stuff
//...

} //end of Bar()

这段代码当然没有说明Foo是将obj作为引用还是作为值,以及Foo是否以任何方式修改obj.

当然,如果Foo将obj作为值或const引用,我们就不会有任何问题.

string Foo (const myType & input); //this is fine 
string Foo (myType input); //so is this

但我们不保证这个!函数签名很可能是

string Foo (myType & input); //asking for trouble!!

但是检查我们想要传递obj的每个函数的签名是非常不方便的,那么我们如何指定我们只想将我们的对象传递给承诺不修改它的函数呢?

当然,一种方法是将obj声明为const,但这种方法的问题在于我们失去了灵活性.如果我们想在调用Foo(obj)后修改Bar()中的obj怎么办?

void Bar ()
{

const myType obj; //default constructor

string valuableInfo = Foo(obj); //compiler will complain if Foo's signature doesnt match

//we can't modify obj here :(
//...

} //end of Bar()

明显但不好的解决方案是这样做:

void Bar ()
{

myType obj; //default constructor

const myType immutableObj {obj}; //copy ctr call
//this is expensive and not recommended if obj is big! want to avoid

string valuableInfo = Foo(immutableObj); //will get the valuable Info risk free
// compiler will complain if Foo has inappropriate signature

//do other stuff
//...

} //end of Bar()

那么这里最好的解决方案是什么?有没有办法静态断言Foo对我们传入的对象是非侵入性的?我们可以暂时使obj const(不必创建一个新的const对象)或其他东西吗?



1> T.C...:

在C++ 17中,感谢P0007R1:

foo(std::as_const(obj));

在C++ 17之前,如果你发现自己经常需要这样做,那么自己写一个助手是微不足道的:

template 
constexpr typename std::add_const::type& as_const(T& t) noexcept { return t; }

// prevent accidentally creating an lvalue out of a const rvalue
template void as_const(const T&&) = delete; 

当然,你所做的一切都不能防止有人故意抛弃常量.墨菲,马基雅维利等



2> Brian..:
Foo(static_cast(obj));


@milleniumbug OTOH,`static_cast`可以改变底层类型.`const_cast`不能.
在这种情况下,为什么你更喜欢`static_cast`到`c​​onst_cast`?
@WChargin`static_cast`可以添加constness,但不能删除它,这使得它比`const_cast`更安全

3> Quentin..:

您可以像Brian建议的那样在现场施放它,但您也可以简单地使用const引用:

myType obj;
myType const &cObj = obj;

string valuableInfo = Foo(cObj);

mutate(obj);


@fluffy如果有人以你没有考虑的方式通过`const_cast`篡改你的对象,他们会被直接送到UB土地.正式来说,这是一个非常好的保证.

4> Dragon Energ..:

我会建议一个迂回的解决方案.

当然,如果Foo将obj作为值或const引用,我们就不会有任何问题.

string Foo (const myType & input); //this is fine

string Foo (myType input); // so is this

但我们不保证这个!函数签名很可能是

string Foo (myType & input); //asking for trouble!

我认为这里有些麻烦.我们没有看到的是这个Foo函数的文档:它的接口注释,有意义的名称等.

Foo在我们使用它之前首先要了解这个功能是它具有的副作用.如果我们不知道它将如何处理我们传入的参数而没有一个constness保证(这只是一个弱的保证,如你所指出的那样,并且const_casts你引入的越多越弱),那么我会建议这可能指向一个Foo记录,重载或使用方式的故障.

不管Foo实际上是调用,不管它是rotate,display,clamp,lerp,paint,flip,info,等,它应该清楚它的副作用,他们不应该在重载之间的逻辑电平变化.接口应该对不变量提供比关于它们将要做什么和不做什么的命名常量更稳固的保证.

例如,如果您有这样的界面设计:

/// @return A flipped 's' (no side effects).
Something flip(Something s);

/// Flips 's' (one side effect).
void flip(Something& s);

......这是一个极其引起问题的设计:所有使用它的开发人员的绊网,虫巢/蜂巢,因为过载在副作用方面各不相同.一个不那么令人困惑的设计是这样的:

/// @return A flipped 's' (no side effects).
Something flipped(Something s);

/// Flips 's' (one side effect).
void flip(Something& s);

... flip基于逻辑副作用不会过载的.

如果您遇到过这样的设计并且它超出您的控制范围,我建议将其包装为更加理智的内容,例如引入该flipped功能:

/// @return A flipped 's' (no side effects).
Something flip(Something s);

/// Flips 's' (one side effect).
void flip(Something& s);

/// @return A flipped 's' (no side effects).
Something flipped(Something s)
{
     flip(s);
     return s;
}

...并且使用该flipped函数而不是你清楚地理解它的副作用以及它应该实际做什么,并将继续独立于你传入的参数的可变性.虽然这比引入一个const_cast调用正确的不可变更加迂回函数的重载,它在根部插入混乱的源头,而不是通过强制传递的东西来解决一个非常模糊的设计constness.

constness最好用作未来可能发生的潜在变化的防御机制,而不是发现/强制执行当前的正确行为.当然你可以用保证Foo(obj)不会obj在未来触发副作用的理由来处理它(假设它不在现在),但在界面层面,副作用不应该是不稳定的.这种.如果今天Foo(obj)不修改obj,那肯定不应该明天.至少,在这方面,界面应该是稳定的.

想象一下一个代码库,在这个代码库中,调用abs(x)并不会让你100%确定是否x会被修改,或者至少在将来不会被修改.现在不是达到constness来解决这个问题的时候了:这里的问题完全在接口/设计层面abs.不应该有可变参数重载abs产生副作用.不应该永远是这种甚至10年后该行的话,那应该是你可以不强迫你的参数取决于公司的保证absconst.对于您使用的任何功能,您应该能够具有相似程度的置信度,前提是它甚至可以远程稳定.

因此虽然规则可能有例外,但我建议检查你的接口,确保它们正确记录事物,不会因为你使用的过载产生不同的逻辑副作用而过载,并且在方面是稳定的他们要记录的事情.



5> 小智..:

您无法保证const的任何内容,也不保证参数不会被不合需要地修改.

Foo()可以轻松地const_cast<>()远离const参数.


确实.虽然编写这样的函数本身并不是无效的.现在你真的*问到麻烦,因为第一次用一个实际上是*`const`的对象调用该函数时,你会得到未定义的行为.在C++中,您始终可以通过编写足够糟糕的代码来调用未定义的行为.我认为重点是防止*accidential*错误,而不是防止任何潜在的滥用.后者在C++中是不可能的.
推荐阅读
郑小蒜9299_941611_G
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有