在重载函数的函数参数中使用rvalue引用会创建太多组合

ale*_*exc 19 c++ rvalue-reference c++11

想象一下,你有很多重载的方法(在C++ 11之前)看起来像这样:

class MyClass {
public:
   void f(const MyBigType& a, int id);
   void f(const MyBigType& a, string name);
   void f(const MyBigType& a, int b, int c, int d);
   // ...
};
Run Code Online (Sandbox Code Playgroud)

这个函数复制了a(MyBigType),所以我想通过提供一个版本f来移动a而不是复制它来添加优化.

我的问题是,现在f重载次数将重复:

class MyClass {
public:
   void f(const MyBigType& a, int id);
   void f(const MyBigType& a, string name);
   void f(const MyBigType& a, int b, int c, int d);
   // ...
   void f(MyBigType&& a, int id);
   void f(MyBigType&& a, string name);
   void f(MyBigType&& a, int b, int c, int d);
   // ...
};
Run Code Online (Sandbox Code Playgroud)

如果我有更多可以移动的参数,那么提供所有重载是不切实际的.

有人处理过这个问题吗?是否有一个很好的解决方案/模式来解决这个问题?

谢谢!

Rya*_*ing 13

Herb Sutter在cppcon谈话中谈到类似的事情

这可以做,但可能不应该.您可以使用通用引用和模板来获得效果,但是您希望将类型约束为MyBigType可隐式转换为的类型MyBigType.使用一些tmp技巧,你可以这样做:

class MyClass {
  public:
    template <typename T>
    typename std::enable_if<std::is_convertible<T, MyBigType>::value, void>::type
    f(T&& a, int id);
};
Run Code Online (Sandbox Code Playgroud)

唯一的模板参数将与参数的实际类型匹配,enable_if返回类型不允许不兼容的类型.我会把它一块一块地拆开

std::is_convertible<T, MyBigType>::value
Run Code Online (Sandbox Code Playgroud)

此编译时表达式将评估trueif是否T可以隐式转换为a MyBigType.例如,如果MyBigTypea std::string和T是一个char*表达式将是真的,但如果T是一个int它将是假的.

typename std::enable_if<..., void>::type // where the ... is the above
Run Code Online (Sandbox Code Playgroud)

void如果is_convertible表达式为真,则此表达式将导致.当它为假时,表达式将格式错误,因此模板将被抛出.

在函数体内你需要使用完美转发,如果你计划复制分配或移动分配,身体就会像

{
    this->a_ = std::forward<T>(a);
}
Run Code Online (Sandbox Code Playgroud)

这里有一个coliru活生生的例子了using MyBigType = std::string.正如Herb所说,这个函数不能是虚函数,必须在头文件中实现.与非模板化重载相比,使用错误类型调用获得的错误消息将非常粗糙.


感谢Barry对此建议的评论,为了减少重复,为SFINAE机制创建模板别名可能是个好主意.如果你在课堂上宣布

template <typename T>
using EnableIfIsMyBigType = typename std::enable_if<std::is_convertible<T, MyBigType>::value, void>::type;
Run Code Online (Sandbox Code Playgroud)

然后你可以减少声明

template <typename T>
EnableIfIsMyBigType<T>
f(T&& a, int id);
Run Code Online (Sandbox Code Playgroud)

但是,这假设您的所有重载都具有void返回类型.如果返回类型不同,则可以使用双参数别名

template <typename T, typename R>
using EnableIfIsMyBigType = typename std::enable_if<std::is_convertible<T, MyBigType>::value,R>::type;
Run Code Online (Sandbox Code Playgroud)

然后使用指定的返回类型声明

template <typename T>
EnableIfIsMyBigType<T, void> // void is the return type
f(T&& a, int id);
Run Code Online (Sandbox Code Playgroud)

稍慢的选项是按值获取参数.如果你这样做

class MyClass {
  public:
    void f(MyBigType a, int id) {
        this->a_ = std::move(a); // move assignment
    } 
};
Run Code Online (Sandbox Code Playgroud)

在f传递左值的情况下,它将a从其参数复制构造,然后将其分配给this->a_.在f传递rvalue 的情况下,它将a从参数移动构造,然后移动assign.这种行为的实例就在这里.请注意,我使用-fno-elide-constructors没有该标志的rvalue情况除了移动构造并且仅发生移动分配.

如果对象移动成本很高(std::array例如),这种方法将明显慢于超级优化的第一版.另外,请考虑观看Chris Drew在评论中链接的Herb的演讲中的这部分内容,以了解它何时可能比使用引用更慢.如果您有Scott Meyers的Effective Modern C++副本,他将讨论第41项中的起伏.


Jar*_*d42 9

您可以执行以下操作.

class MyClass {
public:
   void f(MyBigType a, int id) { this->a = std::move(a); /*...*/ }
   void f(MyBigType a, string name);
   void f(MyBigType a, int b, int c, int d);
   // ...
};
Run Code Online (Sandbox Code Playgroud)

你只需要一个额外的move(可以优化).

  • 我不确定为什么这会被投票.这个问题有两个正确的答案,这绝对是其中之一. (6认同)
  • @mustafa实际上我相信这很好,它会稍微不那么优化,因为它会在左值的情况下进行复制然后移动,然后在rvalue情况下移动然后移动,但是在任何一个都不会再有了*结果*副本. (6认同)
  • @RyanHaining,它甚至更成问题.重点是通过移动而不是复制来优化.现在,您正在为每个函数调用强制复制一个副本.它无视这一点. (2认同)
  • `f()`不是构造函数,不能使用构造函数初始化列表. (2认同)
  • @MooingDuck如果你不需要优化,那么只需传递const引用.没有代码重复. (2认同)

Mar*_*k B 8

我的第一个想法是你应该改变参数来传递值.这包括复制的现有需要,除了复制发生在调用点而不是在函数中显式.它也允许通过移动结构中的移入能够上下文(或者未命名的临时或通过使用所创建的参数std::move).

  • 另请注意[异常安全保障的含义](http://stackoverflow.com/questions/25667284/sink-arguments-and-move-semantics-for-functions-that-c​​an-fail-strong-exception)功能是能够提供的.进入这样的功能肯定会破坏原件.在某些情况下,如果函数抛出异常,则不希望移动该值.这可能是rvalue重载,但不是pass-by-value. (2认同)