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

Dou*_*oug 16 c++ lvalue rvalue-reference c++11

这是C++ 0x右值引用和临时值的后续问题

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

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

void g(const char * arg)
{
    f(arg);
}
Run Code Online (Sandbox Code Playgroud)

由于隐式临时似乎应该调用移动重载,这种情况发生在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);
}
Run Code Online (Sandbox Code Playgroud)

看来,基于我之前问题的答案,该函数g1是合法的(并且被GCC 4.3-4.5接受,但不被MSVC接受).但是,GCC和MSVC都拒绝,g2因为第13.3.3.1.4/3条禁止左值绑定到rvalue ref参数.我理解这背后的基本原理 - 在N2831"使用右值参考修复安全问题"中对此进行了解释.我也认为GCC可能正如该论文的作者所预期的那样实施该条款,因为GCC的原始补丁是由其中一位作者(Doug Gregor)撰写的.

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

void g2(const std::string & arg)
{
    f(std::string(arg));
}
Run Code Online (Sandbox Code Playgroud)

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

将类类型的表达式转换为相同的类类型给出了精确匹配等级,并且将类类型的表达式转换为该类型的基类转换等级,尽管存在复制/移动的事实为这些情况调用构造函数(即,用户定义的转换函数).

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

动机

我提出这个问题的动机类似于在添加新的c ++ 0x rvalue引用运算符重载时如何减少冗余代码中提出的问题("如何在添加新的c ++ 0x rvalue引用运算符重载时减少冗余代码").

如果你有一个接受许多可能移动的参数的函数,并且如果它可以移动它们(例如工厂函数/构造函数:Object create_object(string, vector<string>, string)等),并且想要适当地移动或复制每个参数,你很快就会开始写一个很大的代码.

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

因此,如果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));
}
Run Code Online (Sandbox Code Playgroud)

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

问题

我的问题是:

  1. 这是对标准的有效解释吗?无论如何,它似乎不是传统的或预期的.
  2. 这是否具有直觉意义?
  3. 我没有看到这个想法有什么问题吗?看起来你可以在没有完全预期的情况下安静地创建副本,但这仍然是C++ 03中的现状.而且,它会产生一些当他们目前没有时,过载是可行的,但我不认为这在实践中是一个问题.
  4. 这是一个足够显着的改进,值得制作例如GCC的实验性补丁吗?

Joh*_*itb 3

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

struct A {
  T t;
  A(T t):t(move(t)) { }
};
Run Code Online (Sandbox Code Playgroud)

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

struct A {
  T t;
  A(T t) { swap(this->t, t); }
};
Run Code Online (Sandbox Code Playgroud)

关于交换版本,我宁愿随波逐流,const T&也不愿那个交换。交换技术的主要优点是异常安全,并且是将副本移近调用者,以便它可以优化临时副本。但是,如果您只是构造对象,那么您必须保存什么?如果构造函数很小,编译器可以查看它并且也可以优化掉副本。

struct A {
  T t;
  A(T const& t):t(t) { }
};
Run Code Online (Sandbox Code Playgroud)

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