Как можно избежать копирования, если вызывающему методу не нужно владеть данными?

Вот проблема, о которой я думал в последнее время. Допустим, наш интерфейс — это функция-член, которая возвращает объект, который является дорогим для копирования и дешевым для перемещения (std :: string, std :: vector, и так далее). Некоторые реализации могут вычислять результат и возвращать временный объект, в то время как другие могут просто возвращать объект-член.

Пример кода для иллюстрации:

// assume the interface is: Vec foo() const
// Vec is cheap to move but expensive to copy

struct RetMember {
Vec foo() const { return m_data; }
Vec m_data;
// some other code
}

struct RetLocal {
Vec foo() const {
Vec local = /*some computation*/;
return local;
}
};

Есть также различные «клиенты». Некоторые только читают данные, некоторые требуют владения.

void only_reads(const Vec&) { /* some code */ }
void requires_ownership(Vec) { /* some code */ }

Код выше скомпонован хорошо, но не так эффективно, как могло бы быть. Вот все комбинации:

RetMember retmem;
RetLocal retloc;

only_reads(retmem.foo()); // unnecessary copy, bad
only_reads(retloc.foo()); // no copy, good

requires_ownership(retmem.foo()); // copy, good
requires_ownership(retloc.foo()); // no copy, good

Какой хороший способ исправить эту ситуацию?

Я придумал два пути, но я уверен, что есть лучшее решение.

В моей первой попытке я написал оболочку DelayedCopy, которая содержит либо значение T, либо указатель на const T. Это очень уродливо, требует дополнительных усилий, вводит избыточные перемещения, мешает копированию и, вероятно, имеет много других проблем.

Моя вторая мысль была стиль продолжения, который работает довольно хорошо, но превращает функции-члены в шаблоны функций-членов. Я знаю, что есть std :: function, но она имеет свои издержки, поэтому в плане производительности она может быть неприемлемой.

Образец кода:

#include <boost/variant/variant.hpp>
#include <cstdio>
#include <iostream>
#include <type_traits>

struct Noisy {

Noisy() = default;
Noisy(const Noisy &) { std::puts("Noisy: copy ctor"); }
Noisy(Noisy &&) { std::puts("Noisy: move ctor"); }

Noisy &operator=(const Noisy &) {
std::puts("Noisy: copy assign");
return *this;
}
Noisy &operator=(Noisy &&) {
std::puts("Noisy: move assign");
return *this;
}
};

template <typename T> struct Borrowed {
explicit Borrowed(const T *ptr) : data_(ptr) {}
const T *get() const { return data_; }

private:
const T *data_;
};

template <typename T> struct DelayedCopy {
private:
using Ptr = Borrowed<T>;
boost::variant<Ptr, T> data_;

static_assert(std::is_move_constructible<T>::value, "");
static_assert(std::is_copy_constructible<T>::value, "");

public:
DelayedCopy() = delete;

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

DelayedCopy(DelayedCopy &&) = default;
DelayedCopy &operator=(DelayedCopy &&) = default;

DelayedCopy(T &&value) : data_(std::move(value)) {}
DelayedCopy(const T &cref) : data_(Borrowed<T>(&cref)) {}

const T &ref() const { return boost::apply_visitor(RefVisitor(), data_); }

friend T take_ownership(DelayedCopy &&cow) {
return boost::apply_visitor(TakeOwnershipVisitor(), cow.data_);
}

private:
struct RefVisitor : public boost::static_visitor<const T &> {
const T &operator()(Borrowed<T> ptr) const { return *ptr.get(); }
const T &operator()(const T &ref) const { return ref; }
};

struct TakeOwnershipVisitor : public boost::static_visitor<T> {
T operator()(Borrowed<T> ptr) const { return T(*ptr.get()); }
T operator()(T &ref) const { return T(std::move(ref)); }
};
};

struct Bar {
Noisy data_;

auto fl() -> DelayedCopy<Noisy> { return Noisy(); }
auto fm() -> DelayedCopy<Noisy> { return data_; }

template <typename Fn> void cpsl(Fn fn) { fn(Noisy()); }
template <typename Fn> void cpsm(Fn fn) { fn(data_); }
};

static void client_observes(const Noisy &) { std::puts(__func__); }
static void client_requires_ownership(Noisy) { std::puts(__func__); }

int main() {
Bar a;

std::puts("DelayedCopy:");
auto afl = a.fl();
auto afm = a.fm();

client_observes(afl.ref());
client_observes(afm.ref());

client_requires_ownership(take_ownership(a.fl()));
client_requires_ownership(take_ownership(a.fm()));

std::puts("\nCPS:");

a.cpsl(client_observes);
a.cpsm(client_observes);

a.cpsl(client_requires_ownership);
a.cpsm(client_requires_ownership);
}

Выход:

DelayedCopy:
Noisy: move ctor
client_observes
client_observes
Noisy: move ctor
Noisy: move ctor
client_requires_ownership
Noisy: copy ctor
client_requires_ownership

CPS:
client_observes
client_observes
client_requires_ownership
Noisy: copy ctor
client_requires_ownership

Существуют ли более эффективные методы передачи значений, которые позволяют избежать лишних копий, но все еще носят общий характер (позволяют возвращать как временные, так и члены данных)?

На заметку: код был скомпилирован с g ++ 5.2 и clang 3.7 в C ++ 11. В C ++ 14 и C ++ 1z DelayedCopy не компилируется, и я не уверен, является ли это моей ошибкой или нет.

0

Решение

Вероятно, существуют тысячи «правильных» способов. Я бы предпочел тот, в котором:

  1. метод, который доставляет ссылку или перемещенный объект, явно указан, поэтому никто не сомневается.
  2. как можно меньше кода для поддержки.
  3. Все кодовые комбинации компилируются и делают разумные вещи.

что-то вроде этого (надуманного) примера:

#include <iostream>
#include <string>
#include <boost/optional.hpp>

// an object that produces (for example) strings
struct universal_producer
{
void produce(std::string s)
{
_current = std::move(s);
// perhaps signal clients that there is something to take here?
}

// allows a consumer to see the string but does not relinquish ownership
const std::string& peek() const {
// will throw an exception if there is nothing to take
return _current.value();
}

// removes the string from the producer and hands it to the consumer
std::string take() // not const
{
std::string result = std::move(_current.value());
_current = boost::none;
return result;
}

boost::optional<std::string> _current;

};

using namespace std;

// prints a string by reference
void say_reference(const std::string& s)
{
cout << s << endl;
}

// prints a string after taking ownership or a copy depending on the call context
void say_copy(std::string s)
{
cout << s << endl;
}

auto main() -> int
{
universal_producer producer;
producer.produce("Hello, World!");

// print by reference
say_reference(producer.peek());

// print a copy but don't take ownership
say_copy(producer.peek());

// take ownership and print
say_copy(producer.take());
// producer now has no string. next peek or take will cause an exception
try {
say_reference(producer.peek());
}
catch(const std::exception& e)
{
cout << "exception: " << e.what() << endl;
}
return 0;
}

ожидаемый результат:

Hello, World!
Hello, World!
Hello, World!
exception: Attempted to access the value of an uninitialized optional object.
1

Другие решения

Других решений пока нет …