Вот проблема, о которой я думал в последнее время. Скажем, наш интерфейс - это функция-член, которая возвращает объект, который стоит скопировать и дешево перемещать (std :: string, std :: vector, et cetera). Некоторые реализации могут вычислять результат и возвращать временный объект, в то время как другие могут просто возвращать объект-член.Каковы хорошие способы избежать копирования, если вызывающему абоненту метода не требуется владение данными?
Пример кода для иллюстрации:
// 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. Это очень уродливо, требует дополнительных усилий, вводит избыточные ходы, мешает копированию и, вероятно, имеет многие другие проблемы.
Моя вторая мысль была continuation-passing style, которая работает довольно хорошо, но превращает функции-члены в шаблоны функций-членов. Я знаю, что есть 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 не компилируется, и я не уверен, является ли это моей ошибкой или нет.
возвращение по значению позволяет либо неявной двигаться или [РВО] (https: // эн. википедия.org/wiki/Return_value_optimization) – NathanOliver
@NathanOliver Это именно то, что я имел в виду при написании «no copy, good», no copy = move или copy elision. – sawyer