【问题标题】:C++ const-reference semantics?C++ 常量引用语义?
【发布时间】:2010-06-11 11:11:58
【问题描述】:

考虑下面的示例应用程序。它展示了我所说的有缺陷的类设计。

#include <iostream>

using namespace std;

struct B
{
 B() : m_value(1) {}

 long m_value;
};

struct A
{
 const B& GetB() const { return m_B; }

 void Foo(const B &b)
 {
  // assert(this != &b);
  m_B.m_value += b.m_value;
  m_B.m_value += b.m_value;
 }

protected:
 B m_B;
};

int main(int argc, char* argv[])
{
 A a;

 cout << "Original value: " << a.GetB().m_value << endl;

 cout << "Expected value: 3" << endl;
 a.Foo(a.GetB());

 cout << "Actual value: " << a.GetB().m_value << endl;

 return 0;
}

输出:
原值:1
期望值:3
实际值:4

显然,程序员被b 的常量所迷惑。错误地将b 指向this,这会产生不良行为。

我的问题:在设计 getter/setter 时应该遵循哪些 const 规则?

我的建议:如果可以通过成员函数通过引用设置成员变量,则永远不要返回对成员变量的引用。因此,要么按值返回按值传递参数。 (无论如何,现代编译器都会优化掉多余的副本。)

【问题讨论】:

  • 我不明白为什么期望值为 3。
  • @Neil:如果你不知道A的内幕...
  • 预期值为 3,因为 a.Foo(B(a.GetB())) 等于 3。
  • 为什么会有这么复杂的例子?在这个简单的 sn-p 中可以看到相同的效果:int a = 1; int const&amp; b = a; a = 4; std::cout &lt;&lt; b;
  • 因为类中的受保护成员可以受到保护。这就是这篇文章的内容。

标签: c++ reference constants


【解决方案1】:

显然,程序员被 b 的 const 性愚弄了

正如有人曾经说过的,你一直在使用这个词。我不认为这意味着你认为它的意思。

Const 表示您无法更改该值。这并不意味着该值不能改变。

如果程序员被其他代码可以改变他们不能改变的东西这一事实所愚弄,他们需要更好的别名基础。

如果程序员被标记“const”听起来有点像“constant”但意思是“只读”这一事实所愚弄,他们需要更好地了解他们所使用的编程语言的语义。

因此,如果您有一个返回 const 引用的 getter,那么它就是您无权更改的对象的别名。这没有说明它的值是否是不可变的。


归根结底,这归结为缺乏封装,并且没有应用得墨忒耳法则。一般来说,不要改变其他对象的状态。向他们发送一条消息,要求他们执行一项操作,这可能(取决于他们自己的实现细节)改变他们的状态。

如果您将B.m_value 设为私有,那么您将无法编写您拥有的Foo。你要么把Foo 变成:

void Foo(const B &b)
{
    m_B.increment_by(b);
    m_B.increment_by(b);
}

void B::increment_by (const B& b)
{
    // assert ( this != &b ) if you like 
    m_value += b.m_value;
}

或者,如果要确保该值是恒定的,请使用临时的

void Foo(B b)
{
    m_B.increment_by(b);
    m_B.increment_by(b);
}

现在,单独增加一个值可能合理也可能不合理,并且很容易在 B::increment_by 中进行测试。您还可以测试 A::Foo 中是否有 &amp;m_b==&amp;b,尽管一旦您有几个级别的对象和引用其他对象而不是值的对象(所以 &amp;a1.b.c == &amp;a2.b.c 并不意味着 &amp;a1.b==@987654329 @ 或&amp;a1==&amp;a2),那么你真的必须知道任何操作都可能有别名。

别名意味着将表达式增加两次与第一次计算表达式时增加表达式的值不同;没有真正的解决办法,而且在大多数系统中,复制数据的成本不值得冒着避免别名的风险。

传递结构最少的参数也很有效。如果 Foo() 需要一个 long 而不是它必须从中获取一个 long 的对象,那么它就不会受到别名的影响,并且您不需要编写不同的 Foo() 来将 m_b 增加 C 的值。

【讨论】:

  • 顺便说一句,Stroustrup 原本想要关键字readonly,但C (!) 委员会坚持将其命名为const
  • 这就是示例说明的内容,是的。显然,我不够清楚。必须遵循一些规则(准则)以确保代码中的错误不会发生。
  • 如果行为基于调用函数时的参数值,则将这些值存储在临时变量中。一些编译器支持restrict 来突出显示别名。但是没有任何简单的指导方针可以避免混叠。除非另有说明,否则假定这些值可能是别名。
【解决方案2】:

我提出了一个稍微不同的解决方案,它有几个优点(尤其是在一个不断增长的多线程世界中)。遵循一个简单的想法,那就是最后“提交”您的更改。

要通过您的示例进行解释,您只需将“A”类更改为:

struct A
{
 const B& GetB() const { return m_B; }

 void Foo(const B &b)
 {
  // copy out what we are going to change;
  int itm_value = m_b.m_value;

  // perform operations on the copy, not our internal value
  itm_value += b.m_value;
  itm_value += b.m_value;

  // copy over final results
  m_B.m_value = itm_value ;
 }

protected:
 B m_B;
};

这里的想法是将所有可查看的内存分配放在最后的当前函数上方,它们几乎不会失败。这样,如果在操作中间抛出错误(例如在这 2 个操作中间有一个除法,如果它恰好为 0),那么我们就不会在中间。

此外,在多线程情况下,您可以完成所有操作,然后在最后检查“提交”之前是否有任何变化(一种乐观的方法,通常会通过并且通常会产生更好的结果结果比锁定整个操作的结构),如果它已更改,您只需丢弃值并重试(或返回一个值说明它已失败,如果它可以做一些事情)。

除此之外,编译器通常可以更好地优化这一点,因为不再需要将正在修改的变量写入内存(我们只强制对要更改的值进行一次读取和一次写入)。这样,编译器可以选择仅将相关数据保存在寄存器中,如果没有缓存未命中,则可以保存 L1 缓存访问。否则编译器可能会使其写入内存,因为它不知道可能发生什么别名(因此它不能确保这些值保持不变,如果它们都是本地的,它知道它不能是别名因为当前函数是唯一知道它的函数)。

发布的原始代码可能会发生很多不同的事情。如果某些编译器(启用了优化)实际上会生成产生“预期”结果的代码,而其他编译器不会,我不会感到惊讶。所有这一切仅仅是因为在 c++ 标准中没有很好地定义变量(不是“易失性”)实际上从内存中写入/读取的点。

【讨论】:

    【解决方案3】:

    这里真正的问题是原子性。 Foo 函数的前提条件是它的参数在使用时不会改变。

    如果例如Foo 已使用值参数 i.s.o 指定。参考参数,不会有任何问题。

    【讨论】:

    • 没错。在选择返回引用或值时,也许应该考虑这些前提条件?
    • @Kristoffer:如果您通过引用传递参数,则很难依赖此前提条件。因此,要么您构建一个不允许使用更改参数的系统(即通过锁定参数等),要么您拍摄参数的快照(如果可以以原子方式完成)。
    【解决方案4】:

    坦率地说,A::Foo() 比你原来的问题更让我感到困惑。反正我看,一定是B::Foo()。在B::Foo() 内部检查this 不会那么古怪。

    否则,我看不出如何指定通用规则来涵盖这种情况。并让队友保持理智。

    根据过去的经验,我会将其视为普通错误并区分两种情况:(1)B 很小,(2)B 很大。如果 B 很小,则只需使 A::getB() 返回一个副本。如果 B 很大,那么您别无选择,只能处理 B 的对象在同一个表达式中可能同时是右值和左值的情况。

    如果您经常遇到此类问题,我会说更简单的规则是始终返回对象的副本而不是引用。因为很多时候,如果对象很大,那么无论如何你都必须以不同的方式处理它。

    【讨论】:

      【解决方案5】:

      我的愚蠢答案,我把它留在这里以防其他人提出同样的坏主意:

      问题是我认为引用的对象不是 const(B const &amp; vs const B &amp;),只有代码中的引用是 const。

      【讨论】:

      • 不。我的代码示例的整个想法是说明在更改 this 指针时不能保证不会更改 const 引用的对象。
      • B const &amp;const B &amp; 完全相同。
      • const 适用于它剩下的任何东西。右边的const 是语法糖。 iow const B constconst BB const 相同
      • @Alex 通过尝试我发现它完全一样。
      • 此外,一个 const 引用没有任何意义,因为它不能被“重置”为另一个对象的别名。
      猜你喜欢
      • 2013-07-12
      • 1970-01-01
      • 2020-12-07
      • 1970-01-01
      • 1970-01-01
      • 2014-03-30
      • 2023-03-25
      • 2011-05-07
      • 2014-03-13
      相关资源
      最近更新 更多