C++11:按值传递、移动语义和继承

4
假设我有一个类,我计划直接将其公开为可实例化的类给程序员使用:
class Base
{
public:
    Base(std::string text) : m_text(std::move(text)) {}
private:
    std::string m_text;
};

目前为止一切顺利。这里不需要一个右值构造函数。 现在,在将来的某个时间点,我决定扩展Base:

class Derived : public Base
{
public:
    Derived(const std::string &text) : Base(text) {}
};

这让我很困扰:在Derived中,我不能按值获取字符串,因为Base已经这样做了 - 我最终会得到2个副本和1个移动。此处的const-reference构造函数还会对rvalue执行不必要的复制。

问题是:如何仅复制+移动一次(就像Base中的简单构造函数一样),而不添加更多构造函数?


4
为什么不能使用 Derived(std::string x) : Base(std::move(x)) { } 这样的语法?或者直接继承基类的构造函数? - Kerrek SB
1
@KerrekSB 把继承构造函数放在回答里,它值得点赞。 - Seth Carnegie
3个回答

8

除非更改类的设计并将其构造函数转换为(可能是SFINAE约束的)模板转发构造函数(Yakk的答案显示如何),否则您不能仅复制和移动一次。

虽然这样做可以在提供rvalue时执行一次移动而不进行任何复制,并且在提供lvalue时执行一次复制而不进行任何移动,但在大多数情况下,这是一种过度设计。

作为基于模板的转发构造函数的替代方案,您可以在您的基类和派生类中提供两个构造函数:一个用于rvalue引用,另一个用于对const的lvalue引用。但同样,在大多数情况下,这是一种不必要的复杂化(并且在参数数量增加时无法很好地扩展,因为所需构造函数的数量会呈指数增长)。

移动std::string与复制指针和整数一样快(在此忽略SSO优化),除非您有真实证据表明这是阻止应用程序满足其性能要求的瓶颈(难以置信),否则您不应该担心它。

因此,只需让您的Derived构造函数无条件地按值接受其参数,并在将其传递给基类的构造函数时移动它:
class Derived : public Base
{
public:
    Derived(std::string text) : Base(std::move(text)) { }
};

如果您希望(或接受)Derive继承所有Base的构造函数,另一种选择是利用C++11的继承构造函数,像这样:

class Derived : public Base
{
public:
    using Base::Base;
//  ^^^^^^^^^^^^^^^^^
};

1
根据标准,即使您定义了常规构造函数,编译器也应该隐式生成移动构造函数。如果您定义了复制构造函数或析构函数等,则不应生成移动构造函数(等等,这是一种简化)。然而,某种程度上,微软并不热衷于遵循标准,并且它永远不会隐式生成移动构造函数。如果您想要它,您必须提供它。话虽如此,我不明白这如何适用于您问题中的示例。我认为您在谈论构造“Derived”和移动“string”,而不是移动“Derived”。 - Andy Prowl
嘿,那真是不该这样做! :-D 回到主题:“Derived a(”A“);”构造std::string(”A“),调用Derived :: Derived(第一次复制),然后调用Base :: Base(第二次复制)+在Base :: Base中进行移动(2次复制,1次移动)[假设没有隐式移动构造函数]。 - shorke
2
如果你在谈论我的例子,那只有一次移动(当从临时字符串初始化Derived::Derived的参数时)加上一次移动(当将该参数传递给Base::Base)再加上一次移动(当将其移动到数据成员中)。所以是3次移动,没有复制。 - Andy Prowl
Andy:他假设没有隐式移动构造函数。@user2414893:嗯,当然。但你应该假设有一个隐式移动构造函数。 - Mooing Duck
我刚在VS2012中运行了一些测试。它确实调用了移动构造函数(除非反汇编有误:D)。看起来我刚学到了关于移动语义的一些东西,谢谢! - shorke
显示剩余5条评论

3

使用SFINAE构造函数实现完美转发:

class Derived : public Base
{
public:
  template<
    typename T,
    typename=typename std::enable_if<
      std::is_constructible<std::string, T&&>::value
    >::type
  >
  Derived(T&& text) : Base(std::forward<T>(text)) {}
};

一个缺点(除了上面构造的疯狂之外)是它会过度参与重载决议:它几乎可以匹配任何可以转换为std::string的参数。因此,如果你有另一个构造函数,它接受一个char const*,如果你传递一个char const*变量,它可能会被忽略,因为T&&可能更匹配。
如果你的DerivedBase有一个operator std::string,那么你就倒霉了...

3

Derived更改为以下内容:

class Derived : public Base
{
public:
    using Base::Base;
};

现在DerivedBase继承构造函数。

有关更多信息,请参见在C ++ 0x中转发所有构造函数


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