c++中的函数如何以最小的复制返回值或引用?

How can a function in C++ return either a value or a reference with minimal copying?

本文关键字:返回值 复制 引用 函数 c++      更新时间:2023-10-16

我有一个函数委托给另外两个函数,根据运行时条件返回引用或值:

X by_value() { ... }
const X& by_reference() { ... }
?? foo(bool b) {
  if (b) {
    return by_value();
  } else {
    return by_reference();
  }
}

我想选择函数的返回类型,以便调用者诱导最小的复制;例如:

const X& x1 = foo(true);   // No copies
const X& x2 = foo(false);  // No copies
X x3 = foo(true);          // No copies, one move (or zero via RVO)
X x4 = foo(false);         // One copy

在除最后一种情况外的所有情况下,都不需要(基于运行时行为)复制返回值。

如果foo的返回类型为X,则在情形2中将有一个额外的副本;但如果返回类型为const X&,则情况1和3为未定义行为。

是否有可能通过返回某种代理来确保上述使用具有最小的副本?


解释:由于"you're doing it wrong"的形式有很大的阻力,我想我应该解释一下原因。

假设我有一个类型为T or function<T()>的数组(意味着该数组的元素要么是类型为T的,要么是返回T的函数)。我所说的数组元素的"值"是指值本身或函数求值时的返回值。

如果这个get_value_of_array(int index)按值返回,那么在数组只包含一个元素的情况下,我被迫进行额外的复制。这是我想避免的。


进一步说明:如果答案是"那是不可能的",那对我来说没关系。我很想看到这一点的证明——理想的形式是"假设有一个类型Proxy<X>解决了你的问题。然后……"

您要查找的是求和类型(也就是说,其可能值为"可能的X加上可能的X const&值"的类型)。

在c++中,通常称为variant。它们通常被实现为一个标签加上一个适当大小和对齐的数组,并且在运行时只保留一个值。或者,它们可以通过动态分配和经典访问者模式实现。

例如,使用Boost。变体,您可以声明您的函数返回boost::variant<X, X const&>(实际示例):

boost::variant<X, X const&> foo(bool b) {
  if (b) {
    return by_value();
  } else {
    return by_reference();
  }
}

我认为这是不可能的,因为调用者是否决定移动或复制返回值(无论是来自代理还是来自您的类本身)是编译时决策,而您想要的是使其成为运行时决策。重载解析不能在运行时发生。

我能看到的唯一出路是让被调用者决定这一点,即通过提供一个T &参数,它可以根据它认为合适的方式移动分配或复制分配。
或者,您可以传递一个aligned_storage<sizeof(T)>缓冲区,并让被调用者在其中构造值,如果您不认为调用者可以期望创建某种类型的"null"实例。

好吧,如果真的想要实现这一点,这里有一个相当难看的方法:

X *foo(bool b, X *value) {
  if (b) {
    *value = get_value();
  } else {
    value = get_pointer_to_value();
  }
  return value;
}

使用例子:

void examplefunc() {
    X local_store;
    X *result;
    result = foo(true, &local_store);
    assert(result == &local_store);
    use_x_value(*x);
    result = foo(false, &local_store);
    assert(result != &local_store);
    use_x_value(*x);
}

上面的方法很麻烦:它需要两个局部变量,并且强制通过指针使用返回值。它还暴露了一个原始指针,它不能很好地转换为智能指针(将local_store放入堆中以允许使用智能指针会使这种方法更加复杂,更不用说增加堆分配的开销)。另外,local_store总是默认构造的,但是如果你不需要让examplefunc可重入,它可以被设置为static(或者在多线程版本中使用线程本地存储)。

所以我很难想象你会在什么地方使用这个。总是只返回一个复制的值(让编译器在可能的情况下处理复制省略),或者总是返回一个引用,或者总是返回一个shared_ptr,这样会更简单。

您的目标是错误的:拥有多个返回类型,其中一个是副本,另一个是引用,这会使函数不可预测。

假设foo是某类a, B的成员函数:

X A::foo() { return X(); }
X foo a = A().foo()

定义良好的

const X& B::foo() { return some_internal_x; }
const X& b = B().foo() 

悬空引用

您可以通过将变量类型传递给get_value_of_array来完成您想要的(仍然有点模糊)。这将允许它返回两种不同的类型,并根据数组成员是函数还是数组进行调整。

struct X
{
    X() { std::cout << "Construct" << std::endl; }
    X(X const&) { std::cout << "Copy" << std::endl; }
    X(X&&) { std::cout << "Move" << std::endl; }
};
const X array;
X function() { return X(); }
template<typename ReturnType>
ReturnType get_value_of_array(bool);
template<>
const X& get_value_of_array<const X&>(bool /*isarray*/)
{
    // if (isarray == false) return the cached result of function()
    return array; // gotta build the example yo!
}
template<>
X get_value_of_array<X>(bool isarray)
{
    return isarray ? array : std::move(function());
}
int main()
{
    // Optimizations may vary.
    const X& x1 = get_value_of_array<decltype(x1)>(true);   // No copies or moves
    const X& x2 = get_value_of_array<decltype(x2)>(false);  // No copies or moves.
    X x3 = get_value_of_array<decltype(x3)>(true);          // One copy, one move.
    X x4 = get_value_of_array<decltype(x4)>(false);         // Two moves.
}

感谢Cheers和hth。

在我写这个答案的时候,它不是一个要求,函数foo的参数应该在运行时计算,或者应该被允许是字面上falsetrue以外的,因此:

#include <utility>
#include <iostream>
namespace my {
    using std::cout; using std::endl;
    class X
    {
    private:
        X& operator=( X const& ) = delete;
    public:
        X() {}
        X( X const& )
        { cout << "Copy" << endl; }
        X( X&& )
        { cout << "Move" << endl; }
    };
}  // my
auto foo_true()
    -> my::X
{ return my::X(); }
auto foo_false()
    -> my::X const&
{ static my::X const static_x; return static_x; }
#define foo( arg ) foo_##arg()
auto main() -> int
{
    using namespace my;
    cout << "A" << endl; const X& x1 = foo(true);   // No copies
    cout << "B" << endl; const X& x2 = foo(false);  // No copies
    cout << "C" << endl; X x3 = foo(true);          // No copies, one move (or zero via RVO)
    cout << "D" << endl; X x4 = foo(false);         // One copy
}