【问题标题】:How to prevent accidentally calling a mutating function on a non-const object?如何防止在非常量对象上意外调用变异函数?
【发布时间】:2015-12-05 07:04:40
【问题描述】:

假设我们有一个 myType 类型的 Object obj,我们想将它传递给函数 Foo,它会返回一些关于 obj 的有价值的信息。函数 Bar 是声明 obj 的地方,也是从那里调用 Foo 的地方:

void Bar ()
{

myType obj; //default constructor

string valuableInfo = Foo(obj); 

//do other stuff
//...

} //end of Bar()

这段sn-p的代码当然并没有过多地说明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 对象)或类似的东西吗?

【问题讨论】:

    标签: c++ c++11


    【解决方案1】:

    C++17,感谢P0007R1

    foo(std::as_const(obj));
    

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

    template<class T> 
    constexpr typename std::add_const<T>::type& as_const(T& t) noexcept { return t; }
    
    // prevent accidentally creating an lvalue out of a const rvalue
    template<class T> void as_const(const T&&) = delete; 
    

    当然,您所做的任何事情都无法防止有人故意抛弃 constness。墨菲、马基雅维利等

    【讨论】:

      【解决方案2】:
      Foo(static_cast<const myType&>(obj));
      

      【讨论】:

      • 为什么在这种情况下你更喜欢static_cast 而不是const_cast
      • @WChargin static_cast 可以添加 constness,但不能删除它,这使得它比const_cast 安全得多
      • @milleniumbug OTOH, static_cast 可以更改底层类型。 const_cast 不能。
      • IMO const_cast 更加清晰。您通过const_cast 添加const 的事实是显而易见的,并且不小心将static_casting 添加到不同的底层类型是一个更大的潜在事故。
      • const_cast 可用于意外删除volatile
      【解决方案3】:

      您可以按照Brian 的建议在现场进行投射,但您也可以简单地使用const 参考:

      myType obj;
      myType const &cObj = obj;
      
      string valuableInfo = Foo(cObj);
      
      mutate(obj);
      

      【讨论】:

      • 如果您在创建cObj 和使用它之间改变obj,这不会调用UB吗?
      • @user2357112 根本没有,你为什么认为它会?
      • const 不保证对象的支持数据不会被修改,它只保证 you 不会修改它。 (感谢const_cast,这甚至不是一个很好的保证。)
      • @fluffy 如果有人通过const_cast 以您没有考虑到的方式篡改您的对象,他们将被直接发送到 UB 土地。正式地说,这是一个很好的保证。
      • @Quentin 当然,但const 仍然是一个漂亮的星期保证 - 您可以将 const 参考发送给事物 A(它保留它)和一个非const 参考给事物 B (这会改变它)。你只知道 A (希望)不会改变它;东西 A 不能保证它不会在上面发生变异。
      【解决方案4】:

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

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

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

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

      但我们不能保证这一点!函数签名可以很好 是

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

      我认为这里有更麻烦的事情。我们没有看到的是这个 Foo 函数的文档:它的接口 cmets、一个有意义的名称等。

      在我们使用这个Foo 函数之前,首先要了解它的副作用。如果我们不知道在没有 constness 保证的情况下传入的参数会发生什么(正如所指出的那样,这只是一个弱保证,并且随着 const_casts 你介绍的越多变得越弱),那么我建议这可能指出Foo 的记录方式、过载方式或使用方式的故障。

      不管Foo到底叫什么,不管是rotatedisplayclamplerppaintflipinfo等等,都应该清楚它的副作用,并且它们不应在重载之间的逻辑级别上有所不同。对于不变量,接口应该比命名常量更坚定地保证它们会做什么和不会做什么。

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

      /// @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 年后都不应该有这种事情发生,这应该是一个坚定的保证,你可以依赖它,而不用强迫你对 abs 的论点是 const。您应该能够对您使用的任何功能有类似程度的信心,只要它是远程稳定的。

      因此,虽然规则可能存在例外情况,但我建议检查您的接口,确保它们正确记录内容,不会以根据您使用的重载产生不同逻辑副作用的方式重载,并且相对于他们被记录在案的事情而言是稳定的。

      【讨论】:

        【解决方案5】:

        你不能用 const 保证任何东西,也不能保证参数不会被不受欢迎的修改。

        Foo() 可以轻松地将const_cast&lt;&gt;() 从参数中去掉const

        【讨论】:

        • 这是一个可怕的想法……你能详细说明一下吗?这是我听过的最狡猾的事情。声明一个使用 const 修饰符接收对象的函数,然后去掉修饰符并搞乱它?
        • 确实如此。虽然编写这样的函数本身并不是无效的。现在您真的自找麻烦,因为第一次使用实际上 const 的对象调用该函数时,您会得到未定义的行为。在 C++ 中,您总是可以通过编写足够糟糕的代码来调用未定义的行为。我认为重点是防止意外错误,而不是防止任何潜在的滥用。后者在 C++ 中是不可能的。
        • 你是多么虚无主义;)
        【解决方案6】:

        您可以使用 const_cast 将其临时设为 const:

        Foo(const_cast<const myType>(obj));
        

        【讨论】:

        • 这是一个很好的答案。付出的代价是一个额外的副本(由于价值转换而不是参考转换),但它为您省去了其他麻烦。
        • 当然,无论如何,一旦您制作了副本,调用修改操作不会对您造成伤害,因此不再需要 const。但是根据制作副本的成本,我会犹豫是否使用它,因为我害怕我会忘记一个函数会修改它的参数。
        【解决方案7】:

        所以可能的话......简单的解释,没有任何保证。我见过大量通过 const 引用获取值而不是对其进行 const 转换的代码。它主要是在函数中使用带有状态的函子。由于函子是带状态的,因此它是通过引用获取的,但是由于在右值引用之前无法将临时值传递给接受非常量引用的函数,因此签名是 const 引用。然后对象是 const cast 并且状态被改变了。

        【讨论】:

        • 我认为编写这样的函数是一个非常糟糕的主意。如果你真的想要这些语义,函子的待修改成员应该声明为mutable。这样,如果使用 const 对象调用函数,您将不会得到未定义的行为。
        • 我并不是说这是个好主意,我是说这样的代码很丰富。
        猜你喜欢
        • 2019-11-20
        • 1970-01-01
        • 2018-06-25
        • 2010-09-29
        • 2019-04-21
        • 2018-08-14
        • 2017-12-04
        • 2021-01-25
        相关资源
        最近更新 更多