正当な理由: Sean Parent のGoing Native 2013の「継承は悪の基本クラス」の講演を見たとき、後から考えると、この問題を解決するのが実際にいかに簡単であるかを実感しました。2013 年のゴーイング ネイティブの他のトークと同様に、この Q/A はトーク全体のほんの一部に過ぎませんが、それを視聴することをお勧めします。
実際には、説明がほとんど必要ないほど単純で、コード自体が物語っています。
struct IUsable {
template<typename T>
IUsable(T value) : m_intf{ new Impl<T>(std::move(value)) } {}
IUsable(IUsable&&) noexcept = default;
IUsable(const IUsable& other) : m_intf{ other.m_intf->clone() } {}
IUsable& operator =(IUsable&&) noexcept = default;
IUsable& operator =(const IUsable& other) { m_intf = other.m_intf->clone(); return *this; }
// actual interface
friend void use(const IUsable&);
private:
struct Intf {
virtual ~Intf() = default;
virtual std::unique_ptr<Intf> clone() const = 0;
// actual interface
virtual void intf_use() const = 0;
};
template<typename T>
struct Impl : Intf {
Impl(T&& value) : m_value(std::move(value)) {}
virtual std::unique_ptr<Intf> clone() const override { return std::unique_ptr<Intf>{ new Impl<T>(*this) }; }
// actual interface
void intf_use() const override { use(m_value); }
private:
T m_value;
};
std::unique_ptr<Intf> m_intf;
};
// ad hoc polymorphic interface
void use(const IUsable& intf) { intf.m_intf->intf_use(); }
// could be further generalized for any container but, hey, you get the drift
template<typename... Args>
void use(const std::vector<IUsable, Args...>& c) {
std::cout << "vector<IUsable>" << std::endl;
for (const auto& i: c) use(i);
std::cout << "End of vector" << std::endl;
}
int main() {
std::vector<IUsable> items;
items.emplace_back(3);
items.emplace_back(std::string{ "world" });
items.emplace_back(items); // copy "items" in its current state
items[0] = std::string{ "hello" };
items[1] = 42;
items.emplace_back(A{});
use(items);
}
// vector<IUsable>
// string = hello
// int = 42
// vector<IUsable>
// int = 3
// string = world
// End of vector
// class A
// End of vector
ご覧のとおり、これは a のかなり単純なラッパーでありunique_ptr<Interface>
、派生した をインスタンス化するテンプレート化されたコンストラクターを備えていますImplementation<T>
。すべての (完全ではない) 厄介な詳細は非公開であり、パブリック インターフェイスはこれ以上きれいにはなりません: ラッパー自体には、構築/コピー/移動以外のメンバー関数はありません。インターフェイスはuse()
、既存のものをオーバーロードするフリー関数として提供されます。
明らかに、 の選択は、オブジェクトのコピーを作成したいときはいつでも呼び出されるunique_ptr
プライベート関数を実装する必要があることを意味します (これにはヒープ割り当てが必要です)。確かに、コピーごとに 1 つのヒープ割り当ては非常に最適ではありませんが、パブリック インターフェイスのいずれかの関数が基になるオブジェクトを変更できる場合 (つまり、非 const参照を取得してそれらを変更した場合)、これは要件です。このようにして、すべてのオブジェクトが一意であることを保証します。したがって、自由に変異することができます。clone()
IUsable
use()
問題のように、オブジェクトが完全に不変である場合 (公開されたインターフェイスだけでなく、オブジェクト全体が常に完全に不変であることを意味します)、悪意のある副作用なしに共有状態を導入できます。これを行う最も簡単な方法は、の代わりに- to-constを使用することです。shared_ptr
unique_ptr
struct IUsableImmutable {
template<typename T>
IUsableImmutable(T value) : m_intf(std::make_shared<const Impl<T>>(std::move(value))) {}
IUsableImmutable(IUsableImmutable&&) noexcept = default;
IUsableImmutable(const IUsableImmutable&) noexcept = default;
IUsableImmutable& operator =(IUsableImmutable&&) noexcept = default;
IUsableImmutable& operator =(const IUsableImmutable&) noexcept = default;
// actual interface
friend void use(const IUsableImmutable&);
private:
struct Intf {
virtual ~Intf() = default;
// actual interface
virtual void intf_use() const = 0;
};
template<typename T>
struct Impl : Intf {
Impl(T&& value) : m_value(std::move(value)) {}
// actual interface
void intf_use() const override { use(m_value); }
private:
const T m_value;
};
std::shared_ptr<const Intf> m_intf;
};
// ad hoc polymorphic interface
void use(const IUsableImmutable& intf) { intf.m_intf->intf_use(); }
// could be further generalized for any container but, hey, you get the drift
template<typename... Args>
void use(const std::vector<IUsableImmutable, Args...>& c) {
std::cout << "vector<IUsableImmutable>" << std::endl;
for (const auto& i: c) use(i);
std::cout << "End of vector" << std::endl;
}
関数がどのように消えたかに注意してclone()
ください (もう必要ありません。基礎となるオブジェクトを共有するだけで、不変なので問題ありません) 。保証noexcept
のおかげでコピーがどのようになったかに注目してください。shared_ptr
おもしろいのは、基礎となるオブジェクトは不変でなければならないということですが、それでもIUsableImmutable
ラッパーを変更できるので、これを行ってもまったく問題ありません。
std::vector<IUsableImmutable> items;
items.emplace_back(3);
items[0] = std::string{ "hello" };
(shared_ptr
基になるオブジェクト自体ではなく、 のみが変更されるため、他の共有参照には影響しません)