在重载函数的函数参数中使用右值引用会产生太多组合

Use of rvalue references in function parameter of overloaded function creates too many combinations

假设您有许多重载方法(在 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);
   // ...
};

此函数复制了 a (MyBigType),因此我想通过提供一个移动 a 而不是 af 版本来添加优化正在复制。

我的问题是现在 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);
   // ...
};

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

有人处理过这个问题吗?有没有好的solution/pattern解决这个问题?

谢谢!

如果 move 版本将提供任何优化,那么 move 重载函数和复制函数的实现必须完全不同。如果不为两者提供实现,我看不到解决这个问题的方法。

您可以执行以下操作。

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);
   // ...
};

你刚好有一个额外的move(可能会被优化)。

Herb Sutter 在 a cppcon talk

中谈到了类似的事情

这可以做到,但可能不应该。您可以使用通用引用和模板来获得效果,但您希望将类型限制为 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);
};

唯一的模板参数将匹配参数的实际类型,enable_if return 类型不允许不兼容的类型。我会一块一块拆开的

std::is_convertible<T, MyBigType>::value

如果 T 可以隐式转换为 MyBigType,则此编译时表达式将计算为 true。例如,如果 MyBigTypestd::string,T 是 char*,则表达式为真,但如果 T 是 int,则表达式为假。

typename std::enable_if<..., void>::type // where the ... is the above

如果 is_convertible 表达式为真,此表达式将导致 void。当它为假时,表达式将是畸形的,因此模板将被丢弃。

在函数的 body 中你需要使用完美转发,如果你打算复制分配或移动分配,body 将类似于

{
    this->a_ = std::forward<T>(a);
}

这是一个 coliru live example 和一个 using MyBigType = std::string。正如 Herb 所说,这个函数不能是虚拟的,必须在 header 中实现。与 non-templated 重载相比,使用错误类型调用所获得的错误消息将非常粗糙。


感谢 Barry 对此建议的评论,为了减少重复,为 SFINAE 机制创建一个模板别名可能是个好主意。如果您在 class

中声明
template <typename T>
using EnableIfIsMyBigType = typename std::enable_if<std::is_convertible<T, MyBigType>::value, void>::type;

那么你可以将声明减少到

template <typename T>
EnableIfIsMyBigType<T>
f(T&& a, int id);

但是,这假设您的所有重载都具有 void return 类型。如果 return 类型不同,您可以使用 two-argument 别名代替

template <typename T, typename R>
using EnableIfIsMyBigType = typename std::enable_if<std::is_convertible<T, MyBigType>::value,R>::type;

然后用指定的return类型声明

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


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

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

在向 f 传递左值的情况下,它将从其参数复制构造 a,然后将其赋值给 this->a_。在向 f 传递右值的情况下,它将从参数中移动构造 a,然后移动赋值。这种行为的一个活生生的例子是 here。请注意,我使用 -fno-elide-constructors,没有那个标志,右值情况省略了移动构造,只发生移动赋值。

如果 object 的移动成本很高(例如 std::array),此方法将明显慢于 super-optimized 第一个版本。另外,考虑看一下 this part of Herb's talk that Chris Drew links to in the comments to understand when it could be slower than using references. If you have a copy of Effective Modern C++ by Scott Meyers,他讨论了项目 41 中的起伏。

您可能会引入一个可变对象:

#include <memory>
#include <type_traits>

// Mutable
// =======

template <typename T>
class Mutable
{
    public:
    Mutable(const T& value) : m_ptr(new(m_storage) T(value)) {}
    Mutable(T& value) : m_ptr(&value) {}
    Mutable(T&& value) : m_ptr(new(m_storage) T(std::move(value))) {}
    ~Mutable() {
        auto storage = reinterpret_cast<T*>(m_storage);
        if(m_ptr == storage)
            m_ptr->~T();
    }

    Mutable(const Mutable&) = delete;
    Mutable& operator = (const Mutable&) = delete;

    const T* operator -> () const { return m_ptr; }
    T* operator -> () { return m_ptr; }
    const T& operator * () const { return *m_ptr; }
    T& operator * () { return *m_ptr; }

    private:
    T* m_ptr;
    char m_storage[sizeof(T)];
 };


// Usage
// =====

#include <iostream>
struct X
{
    int value = 0;

    X() { std::cout << "default\n"; }
    X(const X&) { std::cout << "copy\n"; }
    X(X&&) { std::cout << "move\n"; }
    X& operator = (const X&) { std::cout << "assign copy\n"; return *this; }
    X& operator = (X&&) { std::cout << "assign move\n"; return *this; }
    ~X() { std::cout << "destruct " << value << "\n"; }
};

X make_x() { return X(); }

void fn(Mutable<X>&& x) {
    x->value = 1;
}

int main()
{
    const X x0;
    std::cout << "0:\n";
    fn(x0);
    std::cout << "1:\n";
    X x1;
    fn(x1);
    std::cout << "2:\n";
    fn(make_x());
    std::cout << "End\n";
}

我的第一个想法是,您应该将参数更改为按值传递。这涵盖了现有的复制需求,除了复制发生在调用点而不是显式地发生在函数中。它允许在可移动上下文中通过移动构造创建参数(未命名的临时对象或使用std::move)。

为什么你会那样做

这些额外的重载只有在函数实现中修改函数参数确实给您带来显着的性能提升(或某种保证)时才有意义。除了构造函数或赋值运算符之外,几乎不会出现这种情况。因此,我建议您重新考虑,是否真的有必要将这些重载放在那里。

如果实现几乎相同...

根据我的经验,此修改只是将参数传递给包装在 std::move() 中的另一个函数,函数的其余部分与 const & 版本相同。在那种情况下,您可以将您的函数变成这种模板:

template <typename T> void f(T && a, int id);

然后在函数实现中,您只需将 std::move(a) 操作替换为 std::forward<T>(a) 即可,它应该可以工作。如果愿意,您可以将参数类型 T 限制为 std::enable_if

在 const ref 情况下:不要创建临时文件,只是为了修改它

如果在常量引用的情况下,您创建了参数的副本,然后以与移动版本相同的方式继续,那么您也可以按值传递参数并使用与用于移动版本。

void f( MyBigData a, int id );

这通常会在两种情况下为您提供相同的性能,您只需要一个重载和实现。很多优点!

显着不同的实现

如果这两个实现有很大差异,据我所知没有通用的解决方案。而且我相信可以有none。这也是唯一真正有意义的情况,如果分析性能表明您有足够的改进。

这是问题的关键部分:

This function makes a copy of a (MyBigType),

不幸的是,它有点模棱两可。我们想知道参数中数据的最终目标是什么。是吗:

  • 1) 要分配 给调用 f 之前存在的对象?
  • 2) 或者存储在局部变量中:

即:

void f(??? a, int id) {
    this->x = ??? a ???;
    ...
}

void f(??? a, int id) {
    MyBigType a_copy = ??? a ???;
    ...
}

有时,第一个版本(作业)可以在没有任何副本或移动的情况下完成。如果 this->x 已经很长 string,如果 a 很短,那么它可以有效地重用现有容量。没有复制构造,也没有移动。简而言之,有时赋值会更快,因为我们可以跳过复制构造。


无论如何,这里是:

template<typename T>
void f(T&& a, int id) {
   this->x = std::forward<T>(a);  // is assigning
   MyBigType local = std::forward<T>(a); // if move/copy constructing
}