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

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

本文关键字:函数 引用 太多 组合 创建 重载 参数      更新时间:2023-10-16

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

此函数复制aMyBigType ),因此我想通过提供移动a而不是复制它的f版本来添加优化。

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

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

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

谢谢!

赫伯·萨特(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);
};

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

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

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

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

is_convertible表达式为 true 的情况下,此表达式将导致void。 当它为 false 时,表达式将格式不正确,因此模板将被抛出。

在函数的主体中,您需要使用完美的转发,如果您计划进行复制分配或移动分配,则主体将类似于

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

这是一个带有using MyBigType = std::string的 coliru 现场示例。 正如 Herb 所说,这个函数不能是虚拟的,必须在标头中实现。 与非模板化重载相比,使用错误类型调用时收到的错误消息将非常粗糙。


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

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返回类型。 如果返回类型不同,则可以改用双参数别名

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

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

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,然后移动赋值。 这是此行为的一个活生生的例子。 请注意,我使用 -fno-elide-constructors ,如果没有该标志,右值大小写会忽略移动构造,并且只发生移动分配。

如果对象的移动成本很高(例如std::array),这种方法将明显比超级优化的第一个版本慢。 另外,考虑观看克里斯·德鲁(Chris Drew)在评论中链接到的赫伯演讲的这一部分,以了解何时可能比使用参考文献慢。如果你有一本斯科特·迈耶斯(Scott Meyers)的《有效的现代C++》,他会讨论第41项中的起起落落。

您可以执行以下操作。

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(可能会进行优化)。

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

为什么要这样做

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

如果实现几乎相同...

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

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

然后在函数实现中,您只需将std::move(a)操作替换为std::forward<T>(a)它就可以工作。如果您愿意,可以使用 std::enable_if 来约束参数类型T

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

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

void f( MyBigData a, int id );

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

明显不同的实现

如果两种实现存在显着差异,据我所知,没有通用解决方案。我相信不可能有。这也是唯一一种情况,如果分析性能可以显示足够的改进,那么这样做确实有意义。

你可以引入一个可变对象:

#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 << "defaultn"; }
    X(const X&) { std::cout << "copyn"; }
    X(X&&) { std::cout << "moven"; }
    X& operator = (const X&) { std::cout << "assign copyn"; return *this; }
    X& operator = (X&&) { std::cout << "assign moven"; 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 << "Endn";
}

这是问题的关键部分:

此函数复制一个 (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
}

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