1. ホーム
  2. c++

[解決済み] 空のペアの基底クラスを持つ目的は何ですか?

2023-06-11 08:50:19

質問

libstdc++ の pair の実装には、次のような奇妙な点があります。

template<typename, typename> class __pair_base
  {
    template<typename T, typename U> friend struct pair;
    __pair_base() = default;
    ~__pair_base() = default;
    __pair_base(const __pair_base&) = default;
    __pair_base& operator=(const __pair_base&) = delete;
  };

template<typename T, typename U>
  struct pair
  : private __pair_base<T, U>
{ /* never uses __pair_base */ };

__pair_base は決して使われることはなく、また空であることを考えると、それもありえません。これは、特に std::pair 必須 を条件付きで構造化する

pair<T, U> が構造型である場合 T であり U はどちらも構造型です。

プライベートベースを持つことで非構造型になるし。

どのように解決するのですか?

tl;dr これは、非常識なオーバーロード/明示規則を実装するための、実に長い一連のハックの結果です。 std::pair を実装し、ABI 互換性を維持するための、本当に長い一連のハックの結果です。これは、C++20 のバグです。


免責事項

これは、言語レベルの洞察に満ちた啓示というよりも、標準ライブラリの作者と一緒に記憶をたどる楽しみのようなものです。これは、C++ がいかに非常に複雑になったかを示しています。 ペア を実装することがいかに困難な作業であるかがわかります。

一生懸命に歴史を再現してみましたが、私は作者の一人ではありません。

ペア・プライマー

std::pair は単に

template<typename T, typename U>
struct pair
{
    T first;
    U second;
};

には8種類のコンストラクタがリストアップされています。 cppreference に記載されている 8 種類のコンストラクタがありますが、実装者にとってはそれ以上です。すべての条件付き明示的コンストラクタは、実際には 2 ということになります。

これらのコンストラクタのすべてがオーバーロードの解決に関与しているわけではありませんが、もしそうであれば、いたるところに曖昧さが生じるでしょう。その代わり、それぞれがいつオーバーロードを行うかを管理する多くのルールがあります。 すべての の組み合わせは、SFINAE が手動で記述して無効にする必要があります。

この結果、数年にわたり、5 つのバグ報告が コンストラクタだけで . 現在では 6 件になろうとしています;)

プロローグ

その 最初のバグ は、型が同じである場合のペアパラメータの変換可能性のチェックを短絡させることについてです。

template<typename T> struct B;
template<typename T> struct A
{
    A(A&&) = default;
    A(const B<T> &);
};

template<typename T> struct B
{
    pair<A<T>, int> a;
    B(B&&) = default;
};

どうやら、変換性を早くチェックしすぎると、循環依存性のために移動コンストラクタが削除され、どのように B の中ではまだ不完全なものです。 A .

nonesuch

しかし、これは は SFINAE のプロパティを変更しました。 pair . これを受けて、別の修正が実施された。この実装では、以前は無効だった代入演算子が有効になっていたため、署名の変更により代入演算子を手動でオフにしました

struct nonesuch
{
    nonesuch() = delete;
    ~nonesuch() = delete;
    nonesuch(nonesuch const&) = delete;
    void operator=(nonesuch const&) = delete;
};

// ...
pair& operator=(
    conditional_t<conjunction_v<is_copy_assignable<T>,
                                is_copy_assignable<U>>,
                  const pair&, const nonesuch&>::type)

ここで nonesuch はダミーの型であり、本質的にこのオーバーロードを呼び出せなくしています。あるいはそうなのでしょうか?

no_braces_nonesuch

残念ながら、これまで作成できなかった nonesuch

pair<int, int> p = {};  // succeeds
p = {};  // fails

それでも を中括弧で囲んで初期化します。 . ということは delete は過負荷の解決をしないので、これはハード的に失敗です。

修正方法は no_braces_nonesuch

struct no_braces_nonesuch : nonesuch
{
    explicit no_braces_nonesuch(const no_braces_nonesuch&) = delete;
};

explicit はオーバーロードの解決に参加しないようにします。最後に、割り当ては呼び出し不可能です。それとも...?

__pair_base v1

ありますね、残念ながら。 を初期化する別の方法があります。 未知の型

struct anything
{
    template<typename T>
    operator T() { return {}; }
};

anything a;
pair<int, int> p;
p = a;

著者らは、デフォルトで生成される特殊なメンバ関数を利用することで、これを簡単に解決できることに気づきました:割り当て不可能なベースがある場合、これらをまったく宣言しないことができます

class __pair_base
  {
    template<typename T, typename U> friend struct pair;
    __pair_base() = default;
    ~__pair_base() = default;
    __pair_base(const __pair_base&) = default;
    __pair_base& operator=(const __pair_base&) = delete;
  };

すべてのユニットテストに合格し、物事は明るく見えています。知らぬ間に、邪悪なバグの影が地平線上に不気味に立ち込めています。

__pair_base v2

ABIが壊れました。

そんなことがあり得るのか? 空のベースは最適化されます されるのでしょうか?まあ、違いますね。

pair<pair<int, int>, int> p;

残念ながら、空ベース最適化はベースクラスのサブオブジェクトが同じ型の他のサブオブジェクトと重複していない場合にのみ適用されます。この場合 __pair_base は外側のペアと重なります。

修正方法は簡単です。 __pair_base をテンプレート化し、それらが異なる型であることを保証します。

構造的な種類

C++20 が登場し、ペアが 構造型 . このため、プライベートベースが存在しないことが要求されます。

template<pair<int, int>>
struct S;  // fails

というわけで、私たちの旅は終わりです。このことから、私は Chandler Carruth の cppcon での簡単な調査です。 もし必要なら、誰が 1 年で C++ コンパイラを作ることができるでしょうか?どうやら、私は実装の仕方さえ知らないようです。 std::pair .