ここ最近考えていた問題です。インターフェイスが、コピーにコストがかかり、移動にコストがかかるオブジェクトを返すメンバー関数であるとしましょう (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
この状況を修正する良い方法は何ですか?
私は2つの方法を思いつきましたが、もっと良い解決策があると確信しています。
私の最初の試みでは、T の値または const T へのポインターのいずれかを保持する DelayedCopy ラッパーを作成しました。これは非常に見苦しく、余分な労力が必要であり、余分な移動が発生し、コピーの省略が妨げられ、おそらく他にも多くの問題があります。
2 番目に考えたのは、継続渡しスタイルです。これは非常にうまく機能しますが、メンバー関数をメンバー関数テンプレートに変換します。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
余分なコピーを回避する値を渡すためのより良い手法はありますか?
余談ですが、コードは C++11 の g++ 5.2 および clang 3.7 でコンパイルされています。C++14 および C++1z では、DelayedCopy はコンパイルされず、それが私のせいかどうかわかりません。