【问题标题】:Taking sink parameters by rvalue reference instead of by value to enforce performant usage of interfaces通过右值引用而不是按值获取接收器参数以强制接口的高性能使用
【发布时间】:2020-08-19 05:04:54
【问题描述】:

在一次代码审查中,我和我的同事正在讨论我正在编写的函数的接口。我们的代码库使用 C++17,我们不使用异常(视频游戏)。

我声称采用 Sink 参数的惯用 C++ 方式将是高性能的,同时还保持接口灵活,允许调用者传递副本或根据需要从拥有的值移动。通过惯用的方式,我的意思是一个函数按值获取参数,或者为 const 左值和右值引用设置了一个过载集(在左值情况下需要少移动一次,代价是一些代码重复)。

struct A {};

class ByValue
{
public:
    ByValue(std::vector<A> v)
        : m_v(std::move(v))
    {}

private:
    std::vector<A> m_v;
};

class RefOverloads
{
public:
    RefOverloads(std::vector<A> const& v)
        : m_v(v)
    {}

    RefOverloads(std::vector<A>&& v)
        : m_v(std::move(v))
    {}

private:
    std::vector<A> m_v;
};

int main()
{
    std::vector<A> v0;
    ByValue value0(v0);
    ByValue value1(std::move(v0));

    std::vector<A> v1;
    RefOverloads ref0(v1);
    RefOverloads ref1(std::move(v1));
}

另一方面,我的同事不喜欢暗中制作昂贵的副本很容易。他希望这些接收器参数始终通过右值引用(没有 const 左值 ref 重载),并且如果调用者希望传递一个副本,他们必须制作一个本地副本并将其移动到函数中。

class RvalueRefOnly
{
public:
    RvalueRefOnly(std::vector<A>&& v)
        : m_v(std::move(v))
    {}

private:
    std::vector<A> m_v;
};

int main()
{
    std::vector<A> v;
    //RvalueRefOnly failedCopy(v); // Fails purposefully.

    std::vector<A> vCopy = v;               // Explicit copy of v.
    RvalueRefOnly okCopy(std::move(vCopy)); // Move into okCopy.
}

我从来没有想过这样的界面。我的一个反驳观点是,按价值获取更好地表达意图,即带有签名

void f(T x);

调用者知道f 已获得x 的所有权。与

void g(T&& x);

g 可能有所有权,也可能没有,这取决于f 的实现。

有没有最好的方法?我是否以某种方式遗漏了一些论点?

【问题讨论】:

  • 在大多数情况下,按值传递将由您的编译器完美优化,并且值语义对于屏幕编写代码的人来说更容易推理。如果可能,我会通过价值传递,除非您的分析器向您显示不这样做的真正理由。除非您有证明需要这样做,否则不要让它变得复杂。
  • 您的最终目标是什么,以获得最佳性能?
  • 这完全是一个 API 设计问题,可能只是见仁见智。与移动语义一起使用时,按值传递可能会同样快。问题是您是否要向用户强加他们制作自己的副本并在他们打算时移动它的要求,以避免在不打算时意外复制。您可以争辩说在副本会造成破坏的情况下更安全,并且您可以争辩说如果这不是常见的情况是不方便的。
  • @JesperJuhl 在大多数情况下,您的编译器会完美地优化传递值 真的吗?我知道按值返回是,但按值传递 AFAIK 在大多数情况下会留下很多性能。
  • 另一种说法:你的同事的论点是 “我的同事不喜欢很容易隐式地制作昂贵的副本。” 你的同事是对的 如果 这种担忧是个问题。这取决于您的项目或产品的具体独特情况。在我看来,这个问题没有客观的答案。

标签: c++


【解决方案1】:

你基本上有那些构造函数选项:

class myclass {
public:
    // #1 myclass(const std::string& s) : s(s) {}
    // #2 myclass(std::string&& s) : s(std::move(s)) {}

    // #3 myclass(std::string s) : s(std::move(s)) {}

    // #4 template <typename T> myclass(T&& t) : s(std::forward<T>(t)) {}

    std::string s;
};

#3 不能与 #1#2 一起出现 -> 模糊调用

然后打电话

std::string s;
myclass A(s);
myclass B(std::string(s));
myclass C(std::move(s));
myclass D("temporary");
myclass E({5, '*'});

以下是复制/移动构造函数的计数。

                        | A | B | C | D | E |
------------------------|---|---|---|---|---|
1                  Copy |<1>| 2 | 1 | 1 | 1 | <.> Denotes best result (by column)
const l-value ref  Move |<0>| 1 | 1 | 0 | 0 |
                  Other |<0>| 0 | 0 | 1 | 1 |
------------------------|---|-v-|-v-|-v-|-v-| B/C/D/E would prefer overload 2
2                  Copy | X |<1>|<0>| 0 |<0>|
r-value ref        Move | X |<1>|<1>| 1 |<1>| X denotes invalid case
                  Other | X |<0>|<0>| 1 |<1>|
------------------------|---|---|---|---|---|
3                  Copy | 1 | 1 | 0 | 0 |<0>|
by value           Move | 1 | 2 | 2 | 1 |<1>|
                  Other | 0 | 0 | 0 | 1 |<1>|
------------------------|---|---|---|---|---|
4                  Copy |<1>|<1>|<0>|<0>| X |
Forwarding ref     Move |<0>|<1>|<1>|<0>| X |
                  Other |<0>|<0>|<0>|<1>| X |
--------------------------------------------/

可能的配置:

  • 仅限#1:处理所有情况,但会临时复制
  • #1/#2:(B/C/D/E 将使用 #2),因此除了就地构造之外的最佳结果
  • 仅限#3:处理所有情况,但会采取额外措施
  • 仅限#4:处理大多数常规情况,获得最佳结果
  • #1/#2/#4:最佳结果(注意#4)与非常量左值完全匹配)
  • #2/#4: 最好的结果
  • 仅限#2:禁止复制,但显式复制 (B) 比 #1/#2 多移动 1 步

如你所见:

  • 转发参考 (#4) 效果最佳。
  • 通过 const ref (#1) 的复制性能最佳,但其他性能较差。
  • Then By Value (#3) 是第二个“最差”,但仅比最好的多出一步。

其他要点:

  • 只有 #1 在 C++11 之前可用(大多数界面中的默认值也是如此)
  • 只有#1 可能意味着没有所有权转让。
  • 仅移动 (#2) 禁止隐式复制
  • By Value #3 的目的是只编写一个重载作为良好的折衷方案。

现在比较#1/#2#2#3

  • 仅移动类型:

  • #1/#2 无关

  • #3 立即下沉。

  • #2 有机会处理异常保证:如果抛出,则不需要消耗对象。

除非您想要保证和/或立即接收,否则我会使用 pass-by-rvalue (#2)。

对于现有的代码库,我会保持一致性。

对于可复制类型:

  • #1/#2 是最有效的,但允许不需要的复制。
  • #3 很方便(而且只有一个额外的移动),但允许不需要的复制,保证接收器。
  • #2 避免不需要的复制。

现在主要是您要保证和允许的:

  • 最佳性能 -> #1/#2
  • 没有(隐式)副本 -> #2
  • 立即/保证接收 -> #3
  • 与仅可移动类型的一致性

对于现有的代码库,我会保持一致性。

【讨论】:

    猜你喜欢
    • 2014-07-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-11-03
    • 1970-01-01
    • 2012-09-14
    • 1970-01-01
    • 2016-10-24
    相关资源
    最近更新 更多