web-dev-qa-db-ja.com

コンストラクターではなく初期化メソッドを使用する理由

新しい会社に入社したばかりで、コードベースの多くはコンストラクターではなく初期化メソッドを使用しています。

struct MyFancyClass : theUberClass
{
    MyFancyClass();
    ~MyFancyClass();
    resultType initMyFancyClass(fancyArgument arg1, classyArgument arg2, 
                                redundantArgument arg3=TODO);
    // several fancy methods...
};

これはタイミングと関係があると私に言った。いくつかのことを行う必要があることafterコンストラクタで失敗する構築。しかし、ほとんどのコンストラクターは空であり、コンストラクターを使用しない理由は実際にはありません。

それでは、C++の魔法使いよ。あなたがコンストラクターではなくinit-methodを使用するのはなぜですか。

46
bastibe

彼らは「タイミング」と言うので、彼らはinit関数がオブジェクト上の仮想関数を呼び出せるようにしたいからだと思います。基本クラスのコンストラクターでは、オブジェクトの派生クラス部分が「まだ存在しない」ため、特に派生クラスで定義された仮想関数にアクセスできないため、これは常にコンストラクターで機能するとは限りません。代わりに、定義されている場合、関数の基本クラスバージョンが呼び出されます。定義されていない場合(関数が純粋仮想であることを意味します)、未定義の動作が発生します。

Init関数のもう1つの一般的な理由は、例外を避けたいという願望ですが、それはかなり昔ながらのプログラミングスタイルです(そして、それが良いアイデアであるかどうかは、それ自体の議論です)。コンストラクターで機能しないものとは関係ありません。何かが失敗した場合、コンストラクターがエラー値を返すことができないという事実とは関係ありません。ですから、あなたの同僚があなたに本当の理由を与えている限り、私はこれがそうではないと思う。

66
Steve Jessop

はい、いくつか考えられますが、一般的には良い考えではありません。

ほとんどの場合、呼び出される理由は、コンストラクターで例外を介してエラーを報告するだけです(これは正しい)が、従来のメソッドではエラーコードを返すことができます。

ただし、適切に設計されたオブジェクト指向コードでは、コンストラクターがクラスの不変式を確立します。デフォルトのコンストラクターを許可することにより、空のクラスを許可するため、「null」クラスと「意味のある」クラスの両方を受け入れるように不変式を変更する必要があります。クラスを使用するたびに、最初にオブジェクトが適切に構築されています...それはひどいです。

それでは、「理由」を明らかにしましょう。

  • virtualメソッドを使用する必要があります。VirtualConstructorイディオムを使用します。
  • やらなければならないことがたくさんあります。そのため、とにかく、やるべきことは、コンストラクタで行うだけです。
  • セットアップが失敗する可能性があります:例外をスローします
  • 部分的に初期化されたオブジェクトを保持したい:コンストラクター内でtry/catchを使用し、オブジェクトフィールドにエラーの原因を設定します。各パブリックメソッドの開始時にassertを忘れずに、使用する前に使用可能です。
  • オブジェクトを再初期化する:コンストラクターから初期化メソッドを呼び出すと、完全に初期化されたオブジェクトを保持しながらコードの重複を回避できます。
  • オブジェクトを再初期化します(2):operator=を使用します(コンパイラ生成バージョンがニーズに合わない場合は、コピーアンドスワップイディオムを使用して実装します)。

前述のように、一般的に、悪い考えです。本当に「void」コンストラクターが必要な場合は、privateにし、Builderメソッドを使用します。 NRVOと同じくらい効率的です...そして、構築が失敗した場合にboost::optional<FancyObject>を返すことができます。

29
Matthieu M.

他にも多くの考えられる理由が挙げられています(そしてこれらのほとんどが一般に良い考えではない理由の適切な説明)。 実際にはタイミングに関係するinitメソッドの(多かれ少なかれ)有効な使用の例を1つ投稿します。

以前のプロジェクトでは、それぞれが階層の一部であり、さまざまな方法で相互参照する多数のサービスクラスとオブジェクトがありました。そのため、通常、ServiceAを作成するには、初期化時に特定のサービス(場合によってはServiceA自体を含む)の存在に既に依存しているサービスコンテナーを必要とする親サービスオブジェクトが必要でした。その理由は、初期化中に、ほとんどのサービスが特定のイベントのリスナーとして他のサービスに登録された、および/または初期化が成功したことを他のサービスに通知したためです。通知の時点で他のサービスが存在していなかった場合、登録は行われなかったため、このサービスは後でアプリケーションの使用中に重要なメッセージを受信しませんでした。 循環依存関係の連鎖を断ち切るを実現するために、コンストラクターとは別に明示的な初期化メソッドを使用する必要があったため、効果的にグローバルサービスの初期化を2フェーズプロセスにするとなりました。

したがって、このイディオムは一般的に従うべきではありませんが、私見にはいくつかの有効な用途があります。ただし、可能な限りコンストラクターを使用して、その使用を最小限に抑えることが最善です。私たちの場合、これはレガシープロジェクトであり、そのアーキテクチャをまだ完全には理解していませんでした。少なくともinitメソッドの使用はサービスクラスに限定されていました。通常のクラスはコンストラクターによって初期化されていました。サービス初期化メソッドの必要性を排除するためにそのアーキテクチャをリファクタリングする方法があるかもしれないと信じていますが、少なくともそれを行う方法がわかりませんでしたプロジェクトの一部)。

16
Péter Török

私が頭の外から考えることができる2つの理由:

  • オブジェクトの作成には、多くの退屈な作業が必要であり、多くの恐ろしく微妙な方法で失敗する可能性があります。短いコンストラクターを使用して失敗しない基本的なものをセットアップし、ユーザーに初期化メソッドを呼び出して大きな仕事をさせると、少なくとも大きな仕事が失敗してもオブジェクトが作成されていることを確認できます。オブジェクトには、initが失敗した正確な方法に関する情報が含まれている可能性があります。または、他の理由で初期化に失敗したオブジェクトを保持することが重要な場合があります。
  • オブジェクトが作成されてからずっと経ってから再初期化する場合があります。このように、オブジェクトを破棄して再作成することなく、初期化メソッドを再度呼び出すだけです。
7
gspr

このような初期化のもう1つの用途は、オブジェクトプールです。基本的には、プールからオブジェクトを要求するだけです。プールには、空白のN個のオブジェクトがすでに作成されています。メンバーを設定するために好きなメソッドを呼び出すことができるのは、呼び出し元です。呼び出し元がオブジェクトを処理すると、プールに破壊するように指示します。利点は、オブジェクトが使用されるまでメモリが保存され、呼び出し元がオブジェクトを初期化する独自の適切なメンバーメソッドを使用できることです。オブジェクトは多くの目的を果たしている可能性がありますが、呼び出し側はすべてを必要とせず、オブジェクトのすべてのメンバーを初期化する必要もないかもしれません。

通常、データベース接続を考えます。プールには多数の接続オブジェクトを含めることができ、呼び出し元はユーザー名、パスワードなどを入力できます。

5
Manoj R

init()関数は、コンパイラが例外をサポートしていない場合、またはターゲットアプリケーションがヒープを使用できない場合に適しています(例外は通常、ヒープを使用して作成および破棄します)。

init()ルーチンは、構築の順序を定義する必要がある場合にも役立ちます。つまり、オブジェクトをグローバルに割り当てる場合、コンストラクターが呼び出される順序は定義されていません。例えば:

[file1.cpp]
some_class instance1; //global instance

[file2.cpp]
other_class must_construct_before_instance1; //global instance

標準では、-instance1のコンストラクターの前にmust_construct_before_instance1のコンストラクターが呼び出されるという保証はありません。ハードウェアに関連付けられている場合、初期化の順序が重要になる場合があります。

5
TRISAbits

特別な場合の詳細:リスナーを作成する場合、リスナーをどこかに登録する必要がある場合があります(シングルトンやGUIなど)。コンストラクターでそれを行うと、コンストラクターが完了していない(完全に失敗する可能性もある)ため、まだ安全ではない自身へのポインター/参照がリークします。すべてのリスナーを収集し、事態が発生したときにイベントを送信し、イベントを送信するシングルトンを想定し、リスナーのリスト(そのうちの1つは私たちが話しているインスタンスです)をループして、それぞれにメッセージを送信します。しかし、このインスタンスはまだコンストラクターの途中なので、呼び出しはあらゆる種類の悪い方法で失敗する可能性があります。この場合、別の関数に登録することは理にかなっています。これは、コンストラクター自体からnot呼び出しを行うことは明らかです(これは目的を完全に無効にします) )が、構築が完了した後、親オブジェクトから。

しかし、それは特定のケースであり、一般的なケースではありません。

1
Kajetan Abt

リソース管理を行うのに便利です。オブジェクトの存続期間が終了したときにリソースを自動的に割り当て解除するデストラクタを持つクラスがあるとします。これらのリソースクラスを保持するクラスもあり、この上位クラスのコンストラクターでそれらを開始するとします。代入演算子を使用してこの上位クラスを開始するとどうなりますか?内容がコピーされると、古い上位クラスがコンテキストから外れ、デストラクタがすべてのリソースクラスに対して呼び出されます。これらのリソースクラスに割り当て中にコピーされたポインターがある場合、これらのポインターはすべて不良ポインターになります。代わりに、上位クラスの個別のinit関数でリソースクラスを開始する場合、割り当て演算子はこれらのクラスを作成および削除する必要がないため、リソースクラスのデストラクタは呼び出されないように完全にバイパスします。これが「タイミング」の要件が意味するものだと思います。

1
Christopher

また、コードサンプルを添付して回答#1にしたいです-

またmsdnが言うので:

仮想メソッドが呼び出されると、メソッドを実行する実際の型は実行時まで選択されません。コンストラクターが仮想メソッドを呼び出すとき、メソッドを呼び出すインスタンスのコンストラクターが実行されていない可能性があります。

例:次の例は、この規則に違反した場合の効果を示しています。テストアプリケーションは、DerivedTypeのインスタンスを作成します。これにより、ベースクラス(BadlyConstructedType)コンストラクターが実行されます。 BadlyConstructedTypeのコンストラクターは、仮想メソッドDoSomethingを誤って呼び出します。出力が示すように、DerivedType.DoSomething()が実行され、DerivedTypeのコンストラクターが実行される前に実行されます。

using System;

namespace UsageLibrary
{
    public class BadlyConstructedType
    {
        protected  string initialized = "No";

        public BadlyConstructedType()
        {
            Console.WriteLine("Calling base ctor.");
            // Violates rule: DoNotCallOverridableMethodsInConstructors.
            DoSomething();
        }
        // This will be overridden in the derived type.
        public virtual void DoSomething()
        {
            Console.WriteLine ("Base DoSomething");
        }
    }

    public class DerivedType : BadlyConstructedType
    {
        public DerivedType ()
        {
            Console.WriteLine("Calling derived ctor.");
            initialized = "Yes";
        }
        public override void DoSomething()
        {
            Console.WriteLine("Derived DoSomething is called - initialized ? {0}", initialized);
        }
    }

    public class TestBadlyConstructedType
    {
        public static void Main()
        {
            DerivedType derivedInstance = new DerivedType();
        }
    }
}

出力:

ベースctorを呼び出します。

派生DoSomethingが呼び出される-初期化されますか?番号

派生したctorを呼び出します。

1
Tarik