web-dev-qa-db-ja.com

プライベートコンストラクターがプライベートコンストラクターではないのはいつですか?

型があり、そのデフォルトのコンストラクタをプライベートにしたいとします。私は次のように書きます:

class C {
    C() = default;
};

int main() {
    C c;           // error: C::C() is private within this context (g++)
                   // error: calling a private constructor of class 'C' (clang++)
                   // error C2248: 'C::C' cannot access private member declared in class 'C' (MSVC)
    auto c2 = C(); // error: as above
}

すごい。

しかし、その後、コンストラクターは私が思っていたほどプライベートではないことがわかりました:

class C {
    C() = default;
};

int main() {
    C c{};         // OK on all compilers
    auto c2 = C{}; // OK on all compilers
}    

これは、非常に驚​​くべき、予想外の、そして明らかに望ましくない振る舞いだと思います。なぜこれでいいのですか?

84
Barry

トリックはC++ 14 8.4.2/5 [dcl.fct.def.default]にあります。

...関数は、ユーザー宣言であり、その最初の宣言で明示的にデフォルト設定または削除されていない場合、user-providedです。 ...

つまり、Cのデフォルトコンストラクターは、最初の宣言で明示的にデフォルト設定されているため、実際にはnotユーザー提供です。そのため、Cにはユーザー提供のコンストラクターがないため、8.5.1/1 [dcl.init.aggr]ごとの集約です。

aggregateは、ユーザー提供のコンストラクター(12.1)、プライベートまたは保護された非静的データメンバーのない配列またはクラス(9節)です(条項11)、基本クラス(条項10)、仮想関数(10.3)はありません。

57
Angew

デフォルトのコンストラクターを呼び出しているのではなく、集約タイプで集約の初期化を使用しています。集約型は、最初に宣言された場所がデフォルトである限り、デフォルトのコンストラクタを持つことができます。

[dcl.init.aggr]/1 から:

集合体は、配列またはクラス(Clause [class])であり、

  • ユーザー提供のコンストラクター([class.ctor])(基本クラスから継承されたもの([namespace.udecl])を含む)、
  • プライベートまたは保護された非静的データメンバーなし(Clause [class.access])、
  • 仮想関数なし([class.virtual])、および
  • 仮想、プライベート、または保護された基本クラスはありません([class.mi])。

および [dcl.fct.def.default]/5

明示的にデフォルト化された関数と暗黙的に宣言された関数は、まとめてデフォルト関数と呼ばれ、実装はそれらの暗黙的な定義([class.ctor] [class.dtor]、[class.copy])を提供します。 。 関数は、ユーザーが宣言し、最初の宣言で明示的にデフォルトまたは削除されていない場合、ユーザーが提供します。ユーザーが明示的にデフォルトを設定した関数(すなわち、最初の宣言後に明示的にデフォルト設定されます)は、明示的にデフォルト設定された時点で定義されます。そのような関数が暗黙的に削除済みとして定義されている場合、プログラムの形式は正しくありません。 [注:関数を最初の宣言後にデフォルトとして宣言すると、進化するコードベースへの安定したバイナリインターフェイスを有効にしながら、効率的な実行と簡潔な定義を提供できます。 —終了ノート]

したがって、集計の要件は次のとおりです。

  • 非公開メンバーはいません
  • 仮想機能なし
  • 仮想または非パブリックベースクラスなし
  • ユーザー提供のコンストラクターは継承されません。そうでない場合は、次のコンストラクターのみが許可されます。
    • 暗黙的に宣言されている、または
    • 同時にデフォルトとして明示的に宣言および定義されます。

Cはこれらの要件をすべて満たしています。

当然、空のデフォルトコンストラクターを提供するか、宣言後にコンストラクターをデフォルトとして定義することにより、この誤ったデフォルトの構築動作を取り除くことができます。

class C {
    C(){}
};
// --or--
class C {
    C();
};
inline C::C() = default;
53
jaggedSpire

Angew's および jaggedSpire's ' 回答は優れており、 c ++ 11 に適用されます。そして c ++ 14 。そして c ++ 17

ただし、 c ++ 2 では、状況が少し変わり、OPの例はコンパイルされなくなります。

class C {
    C() = default;
};

C p;          // always error
auto q = C(); // always error
C r{};        // ok on C++11 thru C++17, error on C++20
auto s = C{}; // ok on C++11 thru C++17, error on C++20

2つの回答で指摘したように、後者の2つの宣言が機能する理由は、Cが集約であり、これが集約の初期化であるためです。ただし、 P1008 の結果として(OPとあまり似ていない動機付けの例を使用)、集約の定義はC++ 20で [dcl.init.aggrから]/1

集約は、配列またはクラス([クラス])であり、

  • nouser-declaredまたは継承されたコンストラクタ([class.ctor])、
  • プライベートまたは保護された直接の非静的データメンバー([class.access])、
  • 仮想関数なし([class.virtual])、および
  • 仮想、プライベート、または保護された基本クラスはありません([class.mi])。

強調鉱山。現在、要件はuser-declaredコンストラクターではありませんが、以前は(両方のユーザーが回答を引用し、 C++ 11)の履歴を表示できるためC++ 14 、および C++ 17 )noユーザー提供のコンストラクター。 Cのデフォルトコンストラクターはユーザーが宣言しますが、ユーザーが提供するものではないため、C++ 20では集約されなくなります。


集約変更のもう1つの例を次に示します。

class A { protected: A() { }; };
struct B : A { B() = default; };
auto x = B{};

Bは、基本クラスを持っているため、C++ 11またはC++ 14の集約ではありませんでした。結果として、 B{}は、Aの保護されたデフォルトコンストラクターへのアクセス権を持つデフォルトコンストラクター(ユーザー宣言ではなくユーザー提供)を呼び出すだけです。

C++ 17では、 P0017 の結果、集約が拡張され、基本クラスが許可されました。 BはC++ 17の集約です。つまり、B{}は、Aサブオブジェクトを含むすべてのサブオブジェクトを初期化する必要がある集約初期化です。ただし、Aのデフォルトコンストラクターは保護されているため、アクセスできないため、この初期化は不正な形式です。

C++ 20では、Bのユーザー宣言コンストラクターのために、再び集約ではなくなります。そのため、B{}は、デフォルトのコンストラクターの呼び出しに戻り、これも整形式の初期化です。

1
Barry