【问题标题】:Why would const-ness of a local variable inhibit move semantics for the returned value?为什么局部变量的 const-ness 会抑制返回值的移动语义?
【发布时间】:2013-04-11 23:13:51
【问题描述】:
struct STest : public boost::noncopyable {
    STest(STest && test) : m_n( std::move(test.m_n) ) {}
    explicit STest(int n) : m_n(n) {}
    int m_n;
};

STest FuncUsingConst(int n) {
    STest const a(n);
    return a;
}

STest FuncWithoutConst(int n) {
    STest a(n);
    return a;
}

void Caller() {
    // 1. compiles just fine and uses move ctor
    STest s1( FuncWithoutConst(17) );

    // 2. does not compile (cannot use move ctor, tries to use copy ctor)
    STest s2( FuncUsingConst(17) );
}

上面的示例说明了在 C++11 中,正如在 Microsoft Visual C++ 2012 中实现的那样,函数的内部细节如何修改其返回类型。直到今天,我的理解是返回类型的声明是程序员需要知道的,以了解如何处理返回值,例如,当作为参数传递给后续函数调用时。 不是这样。

我喜欢在适当的地方创建局部变量const。它帮助我整理思路并清晰地构建算法。但请注意返回声明为const 的变量!即使不再访问该变量(毕竟执行了 return 语句),并且即使声明为 const 的变量早已超出范围(参数表达式的评估已完成),它无法移动,因此将被复制(如果无法复制,则编译失败)。

这个问题与另一个问题Move semantics & returning const values 有关。不同之处在于,在后者中,函数被声明为返回一个const 值。在我的示例中,FuncUsingConst 被声明为返回一个 volatile 临时变量。然而,函数体的实现细节会影响返回值的类型,并决定返回值是否可以作为其他函数的参数。

此行为是否符合标准?
这怎么能被视为有用?

额外问题:考虑到调用和实现可能在不同的翻译单元中,编译器如何在编译时知道差异?


编辑:尝试改写问题。

一个函数的结果怎么可能比声明的返回类型更多?函数声明不足以确定函数返回值的行为,这似乎完全可以接受吗?对我来说,这似乎是 FUBAR 的一个案例,我只是不确定是该责怪标准还是微软的实施。

作为被调用函数的实现者,我不可能知道所有的调用者,更不用说监控调用代码的每一个微小变化。另一方面,作为调用函数的实现者,我不能依赖被调用函数不返回一个恰好在函数实现范围内声明为 const 的变量。

函数声明是一种契约。现在值多少钱?我们在这里讨论的不是语义等价的编译器优化,比如复制省略,这很好,但不会改变代码的含义。不管复制ctor是否被调用确实改变了代码的含义(甚至可以将代码破坏到无法编译的程度,如上图所示)。为了理解我在这里讨论的尴尬,请考虑上面的“奖金问题”。

【问题讨论】:

  • 如果不查看 Std,我会说应该调用复制 ctor 和移动 ctor。您确定调用了复制ctor吗?
  • return语句创建的临时文件只能被省略“当表达式是具有相同cv的非易失性自动对象(函数或catch子句参数除外)的名称时-作为函数返回类型的非限定类型” [class.copy]/31。这里不是这种情况,所以在FuncUsingConst 中,应该复制对象a,然后在调用之后调用move ctor。
  • 我根本不希望调用复制 ctor,也不是在讨论复制省略。我“只是”想查看临时的移动语义,即FuncUsingConst 的返回值,并传递给STest 移动ctor。
  • 我不完全确定我是否在这里正确解释了标准:在我看来,return 语句中使用的类型(a 的类型)必须与函数返回类型,包括用于省略副本的 cv 限定符。如果这是正确的,您的编译器应该抱怨从a 到非常量临时STest 的复制是不允许的。 FuncUsingConst 的返回类型始终是非常量 STest
  • 不确定我是否正确解释了您的问题,但您似乎在问为什么编译器会尝试通过复制构造函数而不是移动构造函数从 FuncUsingConst 的结果初始化 s2?你抱怨这种方式影响函数接口的实现?但这不是正在发生的事情。编译器尝试从 const 局部变量初始化 FuncUsingConst 的结果,但这样做失败。和外界完全没有关系。事实上,即使你从不调用 FuncUsingConst,这段代码也应该编译失败。

标签: c++11 return constants move-semantics rvalue-reference


【解决方案1】:

我喜欢在适当的地方将局部变量设为 const。它可以帮助我整理思路并清晰地构建算法。

这确实是一个好习惯。尽可能使用const。但是,在这里,您不能(如果您希望您的 const 对象被移出)。

您在函数中声明 const 对象这一事实是一个承诺:只要对象还活着,您的对象的状态就永远不会改变 - 换句话说,从来没有它的析构函数被调用。甚至在之前它的析构函数被调用。只要它还活着,const 对象的状态就不会改变。

然而,在这里你以某种方式期望这个对象在它因超出范围而被破坏之前被移动,并且移动正在改变状态。您不能从 const 对象移动 - 即使您不再使用该对象也是如此。

然而,您可以做的是创建一个非const 对象并仅通过对绑定到该对象的const 的引用在您的函数中访问它:

STest FuncUsingConst(int n) {
    STest object_not_to_be_touched_if_not_through_reference(n);
    STest const& a = object_not_to_be_touched_if_not_through_reference;

    // Now work only with a

    return object_not_to_be_touched_if_not_through_reference;
}

通过一些纪律,您可以轻松地强制执行函数在创建对象后不应修改该对象的语义 - 除非在返回时允许从该对象移动。

更新:

正如 cmets 中 balki 所建议的那样,另一种可能性是将常量引用绑定到非常量临时对象(根据 § 12.2/5 将延长其生命周期),并在以下情况下执行 const_cast退货:

STest FuncUsingConst(int n) {
    STest const& a = STest();

    // Now work only with a

    return const_cast<STest&&>(std::move(a));
}

【讨论】:

  • 这个怎么样? const auto&amp;&amp; obj = STest(n);
  • @balki:这仅适用于const_cast,但这是有可能的。谢谢你的想法,我会扩大我的答案
  • @ecatmur: 谢谢你:)
  • @andy-prowl,详细回复请参阅原帖中的编辑。
  • @vschoech:您的移动构造函数接受STest const&amp;&amp; 什么都不做:如果您只有对const 的引用,您认为您将如何实现该构造函数?您无法更改该对象的状态。它是const。不幸的是,目前我没有时间比我已经做的更广泛地回答你的问题。此外,SO 不是一个论坛,您可以在其中解决人们的帖子。如果您不喜欢这个答案,请不要接受。如果您觉得需要对此答案提出更多问题,请发布一个新问题。
【解决方案2】:

如果对象的复制/移动构造函数 [...] 被隐式 odr 使用并且特殊成员函数不可访问,则程序是非良构的

-- n3485 C++ 标准草案 [class.copy]/30

我怀疑您的问题出在 MSVC 2012 上,而不是 C++11 上。

这段代码,即使没有调用它,也是不合法的 C++11:

struct STest {
  STest(STest const&) = delete
  STest(STest && test) : m_n( std::move(test.m_n) ) {}
  explicit STest(int n) : m_n(n) {}
  int m_n;
};

STest FuncUsingConst(int n) {
  STest const a(n);
  return a;
}

因为没有合法的方式将a 转换为返回值。虽然可以省略返回值,但省略返回值并不会消除复制构造函数存在的要求。

如果 MSVC2012 允许 FuncUsingConst 编译,则违反 C++11 标准。

【讨论】:

  • 我已经在对 OP 的评论中说过类似的话,你能帮我解释一下标准对此的说法吗?为return 语句创建的临时变量只能被省略“当表达式是非易失性自动对象的名称[...] 与函数返回类型具有相同的 cv 非限定类型”[class.copy] /31。我不确定“cv-_un_qualified”在这种情况下是什么意思(相同的资格或限定词被剥夺)。
  • @DyP 我读到说“您可以在省略该副本时忽略 cv 限定条件”,但复制省略并没有消除要省略的复制构造存在被省略的要求。我可能错了:我现在正试图破译这方面的标准。我认为 [class.copy]/30 涵盖了该要求。 “如果对象的复制/移动构造函数 [...] 被隐式 odr 使用并且特殊成员函数不可访问,则程序格式错误”
  • m( 你完全正确。[class.temporary]/1 说得很清楚:“即使临时对象的创建未被评估(第 5 条)或以其他方式避免(12.8),所有语义限制都应得到尊重,就好像临时对象已被创建并随后被销毁一样。”因此,是否删除副本无关紧要;因为删除了副本ctor(因此无法访问),所以不允许这样做。跨度>
  • 但这是否意味着如果复制 ctor 可用,您可以省略复制 (RVA)?也就是说,您可以使用STest res = FuncUsingConst(42); 移动声明为const 的对象?
  • @dyp 它没有被移动,它从未存在过。您认为const 实例是非const 对象的const 视图。存在的对象是返回值...不是const!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-02-27
  • 1970-01-01
  • 2023-03-08
  • 2012-01-01
  • 2017-12-19
  • 2021-11-07
  • 1970-01-01
相关资源
最近更新 更多