16

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

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

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 条禁止左值绑定到右值引用参数。我理解这背后的基本原理 - 在 N2831“解决右值引用的安全问题”中对此进行了解释。我还认为 GCC 可能正在按照该论文的作者的意图实施该条款,因为 GCC 的原始补丁是由其中一位作者(Doug Gregor)编写的。

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

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

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

将类类型的表达式转换为相同的类类型被赋予精确匹配等级,而将类类型的表达式转换为该类型的基类被赋予转换等级,尽管事实上复制/移动为这些情况调用构造函数(即,用户定义的转换函数)。

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

动机

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

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

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

因此,如果左值确实通过隐式副本绑定到右值,那么您可以只编写一个重载create_object(legacy_string &&, legacy_vector<legacy_string> &&, legacy_string &&),它或多或少会像提供右值/左值引用重载的所有组合一样工作——作为左值的实际参数将被复制然后绑定对于参数,作为右值的实际参数将直接绑定。

澄清/编辑:我意识到这实际上与按值接受可移动类型的参数相同,例如 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 制作一个实验性补丁吗?
4

2 回答 2

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 条禁止左值绑定到右值引用参数。我理解这背后的基本原理 - 在 N2831“解决右值引用的安全问题”中对此进行了解释。我还认为 GCC 可能正在按照该论文作者的意图实施该条款,因为 GCC 的原始补丁是由其中一位作者(Doug Gregor)编写的......

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

N2831 与这种情况关系不大。

如果您有一个函数接受许多潜在的可移动参数,并且如果可以移动它们(例如工厂函数/构造函数:Object create_object(string, vector, string) 等),并且想要移动或复制每个参数都合适,你很快就开始写很多代码。

并不真地。为什么要写很多代码?几乎没有理由用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 允许您创建与实际参数具有相同“值”的表达式。所以,如果你写这样的东西:

string foo();

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

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

因此,如果左值确实通过隐式副本绑定到右值,那么您可以只编写一个像 create_object(legacy_string &&, legacy_vector &&, legacy_string &&) 这样的重载,它或多或少会像提供右值/左值引用重载的所有组合一样工作。 ..

好吧,它几乎等同于按值获取参数的函数。不开玩笑。

这是一个足够显着的改进,值得为 GCC 制作一个实验性补丁吗?

没有任何改善。

于 2010-05-01T11:02:20.583 回答
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) { }
};

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

于 2010-05-01T10:54:04.027 回答