C ++ 0x右值引用 - lvalues-rvalue绑定

时间:2010-05-01 08:09:54

标签: c++ c++11 rvalue-reference lvalue

这是一个后续问题 C++0x rvalue references and temporaries

在上一个问题中,我询问了这段代码应该如何工作:

void f(const std::string &); //less efficient
void f(std::string &&); //more efficient

void g(const char * arg)
{
    f(arg);
}

似乎可能因为隐式临时而调用移动重载,这种情况发生在GCC中,而不是MSVC(或MSVC的Intellisense中使用的EDG前端)。

这段代码怎么样?

void f(std::string &&); //NB: No const string & overload supplied

void g1(const char * arg)
{
     f(arg);
}
void g2(const std::string & arg)
{
    f(arg);
}

根据前一个问题的答案,似乎函数g1是合法的(并且被GCC 4.3-4.5接受,但不被MSVC接受)。但是,GCC和MSVC都拒绝g2,因为条款13.3.3.1.4 / 3禁止左值绑定到rvalue ref参数。我理解这背后的基本原理 - 在N2831“使用右值参考修复安全问题”中对此进行了解释。我也认为海湾合作委员会可能按照该文章的作者的意图实施该条款,因为GCC的原始补丁是由其中一位作者(Doug Gregor)撰写的。

但是,我不是很直观。对我来说,(a)const string &在概念上更接近于string &&而不是const char *,并且(b)编译器可以在g2中创建一个临时字符串,就好像它是这样写的:

void g2(const std::string & arg)
{
    f(std::string(arg));
}

实际上,有时复制构造函数被认为是隐式转换运算符。从语法上讲,这是通过复制构造函数的形式建议的,标准甚至在第13.3.3.1.2 / 4节中特别提到了这一点,其中派生基本转换的复制构造函数的转换等级高于其他用户定义的转化:

  

将类类型的表达式转换为相同的类类型,并给出完全匹配等级和转换   对于该类型的基类,类类型的表达式给出了转换等级,尽管事实如此   为这些情况调用复制/移动构造函数(即用户定义的转换函数)。

(我假设在将派生类传递给像void h(Base)这样的函数时使用它,它按值获取基类。)

动机

我提出这个问题的动机类似于How to reduce redundant code when adding new c++0x rvalue reference operator overloads中提出的问题(“如何在添加新的c ++ 0x rvalue引用运算符重载时减少冗余代码”)。

如果你有一个接受许多可能移动的参数的函数,并且如果它可以移动它们(例如工厂函数/构造函数:Object create_object(string, vector<string>, string)等),并且想要移动或复制每个参数在适当的情况下,您可以快速开始编写 lot 代码。

如果参数类型是可移动的,那么可以编写一个按值接受参数的版本,如上所述。但是如果参数是(遗留的)不可移动但可交换的类,而不是C ++ 03,并且你无法改变它们,那么写rvalue引用重载会更有效。

因此,如果lvalues通过隐式副本绑定到rvalues,那么你可以只编写一个像create_object(legacy_string &&, legacy_vector<legacy_string> &&, legacy_string &&)这样的重载,它或多或少会像提供rvalue / lvalue引用重载的所有组合一样工作 - 实际的参数是lvalues会被复制,然后绑定到参数,rvalues的实际参数会直接绑定。

澄清/编辑:我意识到这几乎与接受可移动类型的值的参数相同,例如C ++ 0x std :: string和std :: vector(保存的次数)移动构造函数在概念上被调用)。但是,它对于可复制但不可移动的类型并不相同,它包括具有显式定义的复制构造函数的所有C ++ 03类。考虑这个例子:

class legacy_string { legacy_string(const legacy_string &); }; //defined in a header somewhere; not modifiable.

void f(legacy_string s1, legacy_string s2); //A *new* (C++0x) function that wants to move from its arguments where possible, and avoid copying
void g() //A C++0x function as well
{
    legacy_string x(/*initialization*/);
    legacy_string y(/*initialization*/);

    f(std::move(x), std::move(y));
}

如果g调用f,则会复制xy - 我看不到编译器如何移动它们。如果f被声明为接受legacy_string &&个参数,则可以避免调用者在参数上显式调用std::move的那些副本。我不明白这些是如何等同的。

问题

我的问题是:

  1. 这是对标准的有效解释吗?无论如何,它似乎不是传统的或预期的。
  2. 这是否具有直观意义?
  3. 这个想法是否有问题,我没有看到?看起来你可以在没有完全预期的情况下安静地创建副本,但这仍然是C ++ 03中的现状。此外,它当它们目前没有时会使一些过载成为可能,但我认为它在实践中并不是一个问题。
  4. 这是一个足够重要的改进,值得做出如下GCC的实验补丁?

2 个答案:

答案 0 :(得分:4)

  

这段代码怎么样?

void f(std::string &&); //NB: No const string & overload supplied

void g2(const std::string & arg)
{
    f(arg);
}
  

...但是,GCC和MSVC都拒绝g2,因为条款13.3.3.1.4 / 3禁止左值绑定到rvalue ref参数。我理解这背后的基本原理 - 在N2831“使用右值参考修复安全问题”中对此进行了解释。我也认为海湾合作委员会可能正在按照该文章的作者的意图实施该条款,因为GCC的原始补丁是由其中一位作者(Doug Gregor)撰写的。

不,这只是两个编译器拒绝您的代码的原因的一半。另一个原因是您无法使用引用const对象的表达式初始化对非const的引用。所以,即使在N2831之前,这也行不通。根本不需要转换,因为字符串已经是一个字符串。您似乎希望使用string&&之类的string。然后,只需编写函数f,使其按值获取字符串。如果您希望编译器创建一个const字符串左值的临时副本,以便您可以调用一个string&&的函数,那么将字符串除以值或rref之间没有区别,是吗?

N2831与这种情况没什么关系。

  

如果你有一个接受许多可能移动的参数的函数,并且如果它可以移动它们(例如工厂函数/构造函数:Object create_object(string,vector,string)等),并且想要根据需要移动或复制每个参数,您可以快速开始编写大量代码。

不是真的。你为什么要写很多代码?没有理由将所有代码与const& / &&重载混为一谈。您仍然可以使用单个函数,其中包含pass-by-value和pass-by-ref-to-const,具体取决于您要对参数执行的操作。至于工厂,我们的想法是使用完美的转发:

template<class T, class... Args>
unique_ptr<T> make_unique(Args&&... args)
{
    T* ptr = new T(std::forward<Args>(args)...);
    return unique_ptr<T>(ptr);
}

......一切都很好。特殊模板参数推导规则有助于区分左值和右值参数,而std :: forward允许您创建与实际参数具有相同“值 - ness”的表达式。所以,如果你写这样的东西:

string foo();

int main() {
   auto ups = make_unique<string>(foo());
}

foo返回的字符串会自动移动到堆中。

  

因此,如果lvalues通过隐式副本绑定到rvalues,那么你可以只编写一个像create_object(legacy_string&amp;&amp;,legacy_vector&amp;&amp;,legacy_string&amp;&amp;)这样的重载,它或多或少会起作用比如提供rvalue / lvalue引用过载的所有组合......

嗯,它几乎相当于一个按值获取参数的函数。开个玩笑。

  

这是一项足够重大的改进吗? GCC的实验补丁?

没有任何进步。

答案 1 :(得分:3)

我在这个问题上看不出你的观点。如果您有一个可移动的类,那么您只需要一个T版本:

struct A {
  T t;
  A(T t):t(move(t)) { }
};

如果该课程是传统课程但具有高效swap,您可以编写交换版本,或者您可以回退到const T&方式

struct A {
  T t;
  A(T t) { swap(this->t, t); }
};

关于交换版本,我宁愿选择const T&方式而不是交换方式。交换技术的主要优点是异常安全,并且是将副本移动到更靠近调用者的位置,以便它可以优化临时副本。但是,如果你只是构建对象,你有什么需要保存?如果构造函数很小,编译器可以查看它并且也可以优化远离副本。

struct A {
  T t;
  A(T const& t):t(t) { }
};

对我来说,将字符串左值自动转换为自身的右值副本以绑定到右值引用似乎并不正确。右值引用表示它绑定到右值。但是如果你尝试绑定到相同类型的左值,它最好失败。引入隐藏副本以允许这听起来不对我,因为当人们看到X&&并且你传递X左值时,我打赌大多数人会期望没有副本,并且该绑定是直接,如果它的工作原理。最好立即失败,以便用户可以修复他/她的代码。