コンストラクターとキャストの設計に関するガイドライン

少し前のことですが、残念ながら以前のブログ投稿はそれほど多くありませんでした — 00 について書きました このブログ投稿では、16 を使用する可能性が最も高いと仮定しました。 単一引数のコンストラクター。

しかし、暗黙の単一引数コンストラクターが実際に必要になるのはいつでしょうか?

より広い質問を考えてみましょう:ユーザー定義型のキャスト操作をどのように設計すればよいですか?また、コンストラクターをどのように設計すればよいでしょうか?

しかし、最初に何か違うことがあります:キャストとコンストラクターの違いは何ですか?

キャストとコンストラクター

キャストとコンストラクターの違いを尋ねるのはばかげているように思えるかもしれません。

つまり、これはキャストです:

auto i = static_cast<int>(4.0);

そして、これはコンストラクターを呼び出します:

auto my_vector = std::vector<int, my_allocator<int>>(my_alloc);

ただし、同じキャストがコンストラクター呼び出しのように見える場合があります。

auto i = int(4.0);

そして、コンストラクターはキャストのように見えます:

auto my_vector = static_cast<std::vector<int, my_allocator<int>>>(my_alloc);

違いは何ですか?

これは構文上の違いではなく、意味上の違いです。

コンストラクターとは、任意の数の引数を取り、それらの引数を使用して指定された型の新しいオブジェクトを作成する操作です。新しいオブジェクトの値は、引数の値を使用して作成されますが、引数の値と引数の値の間に直接の接続はありません。新しい値です。C++ のコンストラクターは通常、コンストラクター (C++ 言語の機能) を使用して実装されます。ただし、後で説明するように、そうする必要はありません。

キャスト操作もコンストラクターのその定義に従います。ただし、2 つの点で特別です。1 つ目は、返されたものとは異なる型の単一の引数のみを常に受け​​取ることです。2 つ目は、基本的に を変更しません。値 引数の、型だけです。

最後の 1 つについて少し詳しく説明します。この説明のために、値は数字の 4 のような抽象的な概念です。20 37 として保存されている値を取ります 48 を返します オブジェクトはまだ同じ値 — 数字の 4 を含んでいます。値は変更されず、その値の表現のみが変更されました.

もちろん、これが常に可能であるとは限りません。 、値「number 4.1」は 61 に格納できません .これはナローイング キャストの例です。この状況でキャスト操作がどのように動作するか (例外をスローする、「最も近い値」に丸めるなど) は実装次第です。対照的に、ワイド キャストは次のようになります。 70 :89 の可能なすべての値 92 として表すことができます であるため、常に成功します。

C++ のキャストは、通常、変換演算子またはフリー関数で実装されます。ただし、C++ コンストラクターを使用して実装することもできることに注意してください — これは、以前の混乱につながります.

これらの定義を使用すると、次の操作はすべてキャストになります。新しいオブジェクトを作成しますが、格納された値自体は基本的に同じです。

// the double to int example from above
auto i = static_cast<int>(4.0);

// convert the value "Hello World!" from a character array to a `std::string`
std::string str = "Hello World!";

// convert some pointer value to a unique pointer of the same value
// value didn't change, only ownership is new
std::unique_ptr<int> unique_ptr(some_ptr);

// convert the integer value from above to an optional
// again: no change in value, just represented in a new type that can fit an additional value
std::optional<int> my_opt(i);

しかし、ここではコンストラクターを使用しています:

// the vector value from above
auto my_vector = std::vector<int, my_allocator<int>>(my_alloc);

// create a string using an integer and a character
std::string my_string(10, 'a');

// create a string stream using the string from above
std::stringstream stream(my_string);

専門的な話はさておき、C++ でキャストが処理される方法を詳しく見てみましょう。

暗黙の変換

102 とマークされていない単一引数コンストラクター または非 116 変換演算子は、暗黙的な変換で使用できます。基本的に、コンパイラは、ユーザーが何もしなくても型を調整します。場合によっては、気付かないこともあります!

暗黙的な変換は追加の入力を必要としないため、ある時点で偶発的に発生します。したがって、次のプロパティがある場合にのみ、新しい暗黙的な変換を追加してください:

  • それらは広い変換です:前提条件はプログラマが考える必要がありますが、暗黙的な変換は必要ありません。
  • 適度に安い:たくさん使うので、安いのが一番です。
  • 保存された入力のメリットは大きい:疑わしい場合は、新しい暗黙的な変換を追加しないでください。

暗黙的な変換の良い例は 120 です → 135 .比較的安価で、前提条件がなく、146 を取る関数を変更できるはずです。 ある時点で、オプションの 156 を取る関数へ .

否定的な例は 168 です → 178 —それは多くの問題につながります! — または 181195 — null 以外のポインターが必要であり、動的メモリ割り当てのためにコストがかかります。しかし、1 つ目は C から継承されたもので、2 つ目はあまりにも便利です。

そのガイドラインに直接従うのがこれです:

引数が 1 つのコンストラクタ 206 を作成する デフォルトで!

clang-tidy ルール google-explicit-constructor は本当に役に立ちます。

C++ キャスト

C では、ある型のオブジェクトを別の型に変換するための構文は 1 つだけでした:217 より大きくより優れた言語としての .C++ には、4 つの新しい言語が追加されました:

  • 229 - ええ - 「静的」(?) 変換、それが何であれ
  • 238 const ネスの追加/削除用
  • 244 異なる方法で記憶を解釈するため
  • 257 ポリモーフィックなクラス階層に関連する一連の変換

また、C スタイルのキャスト用の新しい構文 261 も備えています。 これはコンストラクター呼び出しのように見えますが、すべての C スタイルの変換を実行できます — ただし、C スタイルのキャストは無視しましょう。C++ キャストで実行できないことは何もしません。

4 つの新しい C++ キャスト操作のうち、気に入ったのは 1 つだけです。どれだと思いますか?

違います、274 です .

「しかし、なぜですか?」、あなたは尋ねます、「288 は邪悪なツールです。使用しないでください。」

これは本当かもしれませんが、297 1 つのことだけを行います:ポインターの型を変更します。他のキャストは一度に複数のことを行います。

305 を検討してください :2 つの類似しているが非常に異なるジョブがあります — constness の追加と constness の削除に使用できます。1 つ目は完全に無害な状況であり、オーバーロードの解決を支援するために使用されることがあります。2 つ目は、そうしないと未定義の動作への危険な道です。何をしているのかわかりませんが、2 つのモードは同じ関数名を共有しています!

C++17 は 311 を追加します constness を追加する無害な方法として、これは良いことですが、20 年遅すぎます。

323 は似ています:一緒に使用される型に応じて、階層を上にキャストしたり、階層を下にキャストしたり、クラス全体にキャストしたり、333 を返したりできます。 これらは別の機能なので、なぜすべてを 1 つにまとめたのでしょうか?それらは 343 であるべきでした 、 354361379

しかし、それらの最悪は 381 です .次の目的で使用できます:

  • 整数型間の変換
  • 浮動小数点型間の変換
  • 整数の間で変換 浮動小数点型
  • 397 の間で変換 とポインタ型
  • 409 の間で変換 およびその基になる整数型
  • (複雑でない™)基本クラスと派生クラス間の変換
  • 左辺値を右辺値に変換 (419 )
  • 適切なコンストラクタまたは変換演算子があれば、任意の 2 つの型の間で変換します

これらはさまざまな変換であり、一部は縮小されています (422433 )、いくつかは広い (443452 .いくつかは安いです (467479 )、一部は高価です (488495 ソースコードのキャストを見ただけでは、セマンティクスを知ることは不可能です.

ある意味では、これは暗黙の変換よりもわずかに優れているだけです。書くプログラマーは「はい、どうぞ」と言う必要がありますが、読むプログラマーにはあまり役に立ちません。501 の呼び出し または 512 526 よりもはるかに表現力があります 、特にユーザー定義型の場合。

そのため、次の目標を設定します:

531 を使用しないでください : 542 を実行する独自の関数を作成する 変換、555567571 これは特にユーザー定義型に当てはまります。以下を参照してください。

繰り返しますが、目標の結果は次のガイドラインです:

584 は使用しないでください 変換を実装するコンストラクタ (そして 591 は使用しないでください) 変換演算子)

もちろん、絶対に 608 を使用してください !フォーム 614 の使用を実際に意図する場所ではありません .

そのルールの注目すべき例外は 628 です :基本的には適切な暗黙の変換を提供するため、638643 動作しますが、650

ユーザー定義の変換の実装

663 を使用していない場合 コンストラクタ、新しい非暗黙的な変換をどのように追加する必要がありますか?

ソース タイプのオブジェクトを受け取り、デスティネーション タイプの新しいオブジェクトを返す関数を使用してください。関数には、コンストラクターまたは変換演算子よりも大きな利点が 1 つあります。 .

上記のように、その名前を使用して、有用なコンテキスト情報を提供できます。

  • これはナローコンバージョンですか、ワイドコンバージョンですか?
  • 幅が狭い場合、エラーが発生した場合の動作は?
  • など

悪い名前は 670 です 、より適切な名前は 681 です — 少なくともそれは狭いことを知らせます。適切な名前は 696 です 、内容も伝えるため エラーの場合です。

変換関数には接頭辞 701 を付ける必要がないことに注意してください .これ以上の名前がない場合、および/またはエラー情報をエンコードする必要のない幅広い変換である場合にのみ使用してください。

C++ コンストラクター

C++ のコンストラクターについては、C++ のキャストよりも肯定的な意見があります。結局のところ、コンストラクターは C++ の最高の機能であるデストラクタのもう半分です。

したがって、このガイドラインで他の人が言ったことを繰り返します:

コンストラクタを追加して、オブジェクトを有効で整形式の状態にする :そのため、それを行うには十分な引数が必要です。

「有効で整形式の状態」とは、オブジェクトが十分に使用できる状態です。たとえば、基本的な getter 関数を呼び出すことができるはずです。

ただし、これは最低限のことです。他のコンストラクターを追加して、オブジェクトを便利な状態にする必要があります。

たとえば、次のコードをご覧ください:

std::string str; // default constructor puts it into a well-formed state

// now set the actual contents
str = "Hello ";
str += std::to_string(42); // `std::to_string` is a cast, BTW

このような方が断然便利です;

std::string str = "Hello " + std::to_string(42);

// str has the actual state already

ただし、これを極端に行うと、次のようになります。

std::vector<int> vec(5, 2);

713 のように 、パラメーターに関する追加情報を提供する余地はありません。これはコンストラクターの問題です。

もう 1 つはこれです:多くの状態で初期化する必要がある何らかの形式の不変オブジェクトを作成しているとします。コンストラクターに大量のパラメーターを渡すべきではありません!

パラメータの意味が明確で、パラメータが多すぎない場合にのみ、コンストラクタを追加してください。

代わりに何をすべきですか?

2 つの選択肢があります。

名前付きコンストラクタ

名前付きコンストラクターはフリー関数または 722 です オブジェクトの構築に使用されるメンバー関数です。繰り返しますが、適切な名前を付けることができます!

たとえば、736 を考えてみましょう class.It には 2 つの主要なコンストラクターがあります。1 つは新しいファイルを作成し、もう 1 つは既存のファイルを開きます。 P>

ただし、別の名前を付けることができます:

class file
{
public:
  static file open(const fs::path& p);
  static file create(const fs::path& p);
};

…

auto f1 = file::open(…);
auto f2 = file::create(…);

ただし、名前付きコンストラクターは通常のコンストラクターほど人間工学的ではありません。745 では使用できません。 、たとえば。

別の実装では、コンストラクターを使用し、単にタグを追加して名前を付けます。現在、それらは emplace のような関数で使用できます。

class file
{
public:
  static constexpr struct open_t {} open;
  file(open_t, const fs::path& p);

  static constexpr struct create_t {} create;
  file(create_t, const fs::path& p);
};

…

auto f1 = file(file::create, …);
auto f2 = file(file::open, …);

名前付きコンストラクタのどの実装を使用するかはあなた次第です。私は 759 を使用する傾向があります 複雑なコンストラクターがある場合は、両方のバリアントのいずれかを使用することを検討する必要があります。

ビルダー パターン

コンストラクターが複雑すぎる場合は、ビルダー パターンが役立ちます。作成関数を 1 つだけ持つ代わりに、クラス全体であるビルダーを使用します。さまざまな属性と 764 を設定する多くの関数が含まれています。 ファイナライズされたオブジェクトを返すメンバー関数。

cppast の複雑なクラスに使用します。変更できないため、すべてのプロパティで完全に作成する必要があります。778 は次のとおりです。 オブジェクト、例:

class cpp_class
{
public:
    class builder
    {
    public:
        // specify properties that always need to be provided
        explicit builder(std::string name, cpp_class_kind kind, bool is_final = false);

        // mark the class as final
        void is_final() noexcept;

        // add a base class
        cpp_base_class& base_class(std::string name, std::unique_ptr<cpp_type> type,
                                   cpp_access_specifier_kind access, bool is_virtual);


        // add a new access specifier
        void access_specifier(cpp_access_specifier_kind access);

        // add a child
        void add_child(std::unique_ptr<cpp_entity> child) noexcept;

        // returns the finished class
        std::unique_ptr<cpp_class> finish(const cpp_entity_index& idx, cpp_entity_id id,
                                          type_safe::optional<cpp_entity_ref> semantic_parent);

    private:
        std::unique_ptr<cpp_class> class_;
    };

    … // but no public constructors
};

ビルダー パターンには、セッター関数をクラスに「インライン化」するよりもいくつかの利点があることに注意してください。

    <リ>

    クラス自体は不変にすることができ、多くのセッターは必要ありません。

    <リ>

    メンバーはデフォルトで構築可能である必要はありません:ビルダーはそれらを 787 として保存できます または 796 808 でアサートします 関数が設定されていることを確認します。その後、実際のクラス オブジェクトを作成できます。

ビルダー パターンの欠点の 1 つは、冗長性が追加されることです。また、作成されたオブジェクトがポリモーフィックではなく、値によって返される場合、ネストされたクラスは、現在作成中のオブジェクトのメンバーを持つことができません。

class foo
{
public:
    class builder
    {
        foo result_; // error: foo is an incomplete type at this point

        …
    };

    …
}:

これを回避するには、ビルダーにすべてのメンバーを個別に含めるか、クラスの外部で定義する必要があります:

class foo
{
public:
  class builder;

  …
};

class foo::builder
{
  foo result_; // okay

  …
};

しかし、それらを除けば、ビルダー パターンは便利なツールです。ただし、まれな状況でしか使用されません。

結論

独自の型を記述するときは、提供するコンストラクターとキャスト操作について考えてください。

特に:

  • 単一引数のコンストラクタ 812 を作成する キャストには使用しないでください
  • 絶対に必要であると確信している場合にのみ、暗黙的な変換を追加してください
  • 適切な名前付きの非メンバー関数としてキャスト操作を実装することを好む
  • パラメータが紛らわしい場合は、名前付きコンストラクタを検討してください
  • 複雑なコンストラクターがある場合は、ビルダー パターンを検討してください

824 も避けるようにしてください 、代わりに特殊なキャスト関数を使用してください。何が行われたかを明確に示すため、より読みやすくなっています。

このルールに従うことで、インターフェイスがより使いやすくなり、その機能がより明確になります。