2

これは、真の答えがないように思われる、やや広い質問です。

かなり長い間、構成されたオブジェクトの初期化について混乱してきました。私は、すべてのメンバー データにゲッターとセッターを提供し、自動オブジェクトではなくオブジェクトへの生のポインターを優先するように正式に教えられまし

では、オブジェクトで構成されたオブジェクトを初期化するにはどうすればよいでしょうか。

これは、学校で学んだことを使用して初期化を試みる方法です。

class SmallObject1 {
public:
    SmallObject1() {};
};

class SmallObject2 {
    public:
        SmallObject2() {};
};

class BigObject {
    private:
        SmallObject1 *obj1;
        SmallObject2 *obj2;
        int field1;
        int field2;
    public:
        BigObject() {}
        BigObject(SmallObject1* obj1, SmallObject2* obj2, int field1, int field2) {
        // Assign values as you would expect
        }
        ~BigObject() {
            delete obj1;
            delete obj2;
        }
    // Apply getters and setters for ALL members here
};

int main() {
    // Create data for BigObject object
    SmallObject1 *obj1 = new SmallObject1();
    SmallObject2 *obj2 = new SmallObject2();
    int field1 = 1;
    int field2 = 2;

    // Using setters
    BigObject *bobj1 = new BigObject();
    // Set obj1, obj2, field1, field2 using setters

    // Using overloaded contructor
    BigObject *bobj2 = new BigObject(obj1, obj2, field1, field2);

    return 0;
}

このデザインは(私にとって)読みやすいので魅力的です。メンバ オブジェクトへのポインタがあるという事実により、初期化および初期化後BigObjectに可能になります。ただし、動的メモリはプログラムをより複雑にし、道を混乱させる可能性があるため、メモリ リークが発生する可能性があります。さらに、ゲッターとセッターを使用するとクラスが乱雑になり、メンバー データへのアクセスや変更が簡単になりすぎる可能性があります。obj1obj2

これは実際に悪い習慣ですか?メンバー オブジェクトをその所有者とは別に初期化する必要がある場合がよくあります。これにより、自動オブジェクトが魅力的ではなくなります。さらに、より大きなオブジェクトが独自のメンバー オブジェクトを構築できるようにすることも検討しました。これは、セキュリティの観点からは理にかなっているように見えますが、オブジェクトの責任の観点からはあまり意味がありません。

4

4 に答える 4

0

他の人は最適化の理由を説明していますが、私は今、タイプ/機能の観点から見ています。Stroustrup によれば、「クラスの不変条件を確立するのは、すべてのコンストラクターの仕事です」。ここでのクラス不変条件は何ですか? 知っておくこと (および定義すること) は重要です。そうしないと、メンバー関数をifs で汚染して、操作が有効かどうかを確認することになります。これは、型をまったく持たないよりもはるかに優れています。90 年代にはそのようなクラスがありましたが、現在では不変の定義に固執し、オブジェクトが常に有効な状態にあることを望んでいます。(関数型プログラミングはさらに一歩進んで、オブジェクトから変数の状態を抽出しようとするため、オブジェクトを const にすることができます。)

  • これらのサブオブジェクトがある場合にクラスが有効な場合は、それらをメンバーとして保持してください。
  • BigObject 間で SmallObjectを共有する場合は、ポインタが必要です。
  • 特定の SmallObject を持たないことが有効であるが、共有する必要がない場合は、std::optional<SmallObject>メンバーを検討できます。Optional は通常、(ヒープに対して) ローカルに割り当てられるため、キャッシュの局所性からメリットが得られる場合があります。
  • このようなオブジェクトを構築するのが難しい場合 (コンストラクターのパラメーターが多すぎるなど) は、構築とクラス メンバーという 2 つの直交する問題があります。ビルダー クラス (ビルダー パターン) を導入して、構築の問題を解決します。通常実行可能な解決策は、すべてのコンストラクターのすべてのパラメーターをオプションのメンバーとして持つことです。

関数型スタイルを好む私たちの多くは、builder をアンチパターンと見なし、逆シリアル化にのみ使用することに注意してください。背後にある理由として、ビルダーについて推論するのは非常に困難です (結果として、成功するかどうか、どのコンストラクターが呼び出されるか)。2 つの int がある場合、それは 2 つの int です。あなたの最善の策は、通常、それらを別々の変数に保持することです。その後、あらゆる種類の最適化を行うのはコンパイラ次第です。ピースが奇跡的にバラバラになり、int が「その場で」構築されても驚かないので、後でコピーする必要はありません。

OTOH、同じパラメーターが他の場所よりも先に多くの場所で「制限される」(値を取得する) ことがわかった場合は、それらの型を導入することができます。この場合、2 つの int は型 (できれば構造体) になります。の基本クラスBigObject、メンバー、または別のクラスにするかどうかを決定できます (複数のバインド順序がある場合は、3 番目を選択する必要があります)。いずれの場合でも、コンストラクターは新しいクラスを取得します。 2 つの int の代わりにクラス。他のコンストラクター (2 つの int を取るもの) を非推奨にすることを検討することもできます。1. 新しいオブジェクトは簡単に作成できる、2. 共有される可能性がある (例: ループ内でアイテムを作成する場合)。古いコンストラクターを保持したい場合は、一方を他方へのデリゲートにします。

于 2016-07-10T15:22:50.403 に答える
0

すべてのメンバー データにゲッターとセッターを提供し、自動オブジェクトではなくオブジェクトへの生のポインターを優先するように正式に教えられました。

残念ながら、あなたは間違って教えられました。

,のような標準ライブラリ構造よりも生のポインターを優先する理由はまったくありません。std::vector<>std::array<>std::unique_ptr<>std::shared_ptr<>

バグのあるソフトウェアの最も一般的な原因は、(独自にロールバックした) メモリ管理が欠陥を明らかにすることであり、さらに悪いことに、これらは通常、デバッグが困難です。

于 2016-07-08T20:22:51.747 に答える
0

次のコードを検討してください。

class SmallObj {
public:
  int i_;
  double j_;
  SmallObj(int i, double j) : i_(i), j_(j) {}
};

class A {
  SmallObj so_;
  int x_;
public:
  A(SmallObj so, int x) : so_(so), x_(x) {}
  int something();
  int sox() const { return so_.i_; }
};

class B {
  SmallObj* so_;
  int x_;
public:
  B(SmallObj* so, int x) : so_(so), x_(x) {}
  ~B() { delete so_; }
  int something();
  int sox() const { return so_->i_; }
};

int a1() {
  A mya(SmallObj(1, 42.), -1.);
  mya.something();
  return mya.sox();
}

int a2() {
  SmallObj so(1, 42.);
  A mya(so, -1.);
  mya.something();
  return mya.sox();
}

int b() {
  SmallObj* so = new SmallObj(1, 42.);
  B myb(so, -1.);
  myb.something();
  return myb.sox();
}

アプローチ「A」の欠点:

  • を具体的に使用するとSmallObject、その定義に依存するようになります。単に前方宣言することはできません。
  • 私たちのインスタンスは私たちのインスタンスにSmallObject固有のものです (共有されていません)。

「B」にアプローチすることの欠点はいくつかあります。

  • 所有権契約を結び、ユーザーにそれを認識させる必要があります。
  • Beveryを作成する前に、動的メモリ割り当てを実行する必要があります。
  • この重要なオブジェクトのメンバーにアクセスするには、間接化が必要です。
  • デフォルトのコンストラクターのケースをサポートする場合は、null ポインターをテストする必要があります。
  • 破壊にはさらに動的メモリ呼び出しが必要です。

自動オブジェクトの使用に反対する理由の 1 つは、それらを値渡しするコストです。

これは疑わしいです: 自明な自動オブジェクトの多くの場合、コンパイラはこの状況に合わせて最適化し、サブオブジェクトをインラインで初期化できます。コンストラクターが単純な場合、1 回のスタック初期化ですべてを実行できる場合もあります。

GCC の a1() の -O3 実装を次に示します。

_Z2a1v:
.LFB11:
  .cfi_startproc
  .cfi_personality 0x3,__gxx_personality_v0
  subq  $40, %rsp      ; <<
  .cfi_def_cfa_offset 48
  movabsq $4631107791820423168, %rsi  ; <<
  movq  %rsp, %rdi     ; <<
  movq  %rsi, 8(%rsp)  ; <<
  movl  $1, (%rsp)     ; <<
  movl  $-1, 16(%rsp)  ; <<
  call  _ZN1A9somethingEv
  movl  (%rsp), %eax
  addq  $40, %rsp
  .cfi_def_cfa_offset 8
  ret
  .cfi_endproc

強調表示された ( ; <<) 行は、A のインプレース構築を行うコンパイラであり、これは SmallObj サブオブジェクトです。

そして a2() は非常によく似た最適化を行います:

_Z2a2v:
.LFB12:
  .cfi_startproc
  .cfi_personality 0x3,__gxx_personality_v0
  subq  $40, %rsp
  .cfi_def_cfa_offset 48
  movabsq $4631107791820423168, %rcx
  movq  %rsp, %rdi
  movq  %rcx, 8(%rsp)
  movl  $1, (%rsp)
  movl  $-1, 16(%rsp)
  call  _ZN1A9somethingEv
  movl  (%rsp), %eax
  addq  $40, %rsp
  .cfi_def_cfa_offset 8
  ret
  .cfi_endproc

そして、b() があります。

_Z1bv:
.LFB16:
        .cfi_startproc
        .cfi_personality 0x3,__gxx_personality_v0
        .cfi_lsda 0x3,.LLSDA16
        pushq   %rbx
        .cfi_def_cfa_offset 16
        .cfi_offset 3, -16
        movl    $16, %edi
        subq    $16, %rsp
        .cfi_def_cfa_offset 32
.LEHB0:
        call    _Znwm
.LEHE0:
        movabsq $4631107791820423168, %rdx
        movl    $1, (%rax)
        movq    %rsp, %rdi
        movq    %rdx, 8(%rax)
        movq    %rax, (%rsp)
        movl    $-1, 8(%rsp)
.LEHB1:
        call    _ZN1B9somethingEv
.LEHE1:
        movq    (%rsp), %rdi
        movl    (%rdi), %ebx
        call    _ZdlPv
        addq    $16, %rsp
        .cfi_remember_state
        .cfi_def_cfa_offset 16
        movl    %ebx, %eax
        popq    %rbx
        .cfi_def_cfa_offset 8
        ret
.L6:
        .cfi_restore_state
.L3:
        movq    (%rsp), %rdi
        movq    %rax, %rbx
        call    _ZdlPv
        movq    %rbx, %rdi
.LEHB2:
        call    _Unwind_Resume
.LEHE2:
        .cfi_endproc

明らかに、この場合、値の代わりにポインタで渡すために多額の代償を払いました。

次のコードを考えてみましょう。

class A {
    SmallObj* so_;
public:
    A(SmallObj* so);
    ~A();
};

class B {
    Database* db_;
public:
    B(Database* db);
    ~B();
};

上記のコードから、A のコンストラクターで "SmallObj" の所有権をどのように期待していますか? また、B の「データベース」の所有権についてどのように期待していますか? 作成するすべての B に対して一意のデータベース接続を構築するつもりですか?

生のポインターを優先するというあなたの質問にさらに答えるには、Cs (文字列のコピーへのポインターを返す、解放することを忘れないでstd::unique_ptrください。 )。std::shared_ptrstrdup()

標準化委員会の前に、C++17 に を導入するという提案がありobserver_ptrます。これは、未加工のポインターの非所有ラッパーです。

これらを好みのアプローチで使用すると、多くのボイラープレートが導入されます。

auto so = std::make_unique<SmallObject>(1, 42.);
A a(std::move(so), -1);

を介して明示的に所有権を付与しているため、割り当てaたインスタンスの所有権が にあることがわかります。しかし、そのすべてが明示的であることはすべて文字を犠牲にします。対比:sostd::move

A a(SmallObject(1, 42.), -1);

また

SmallObject so(1, 4.2);
A a(so, -1);

したがって、合成のために小さなオブジェクトの生のポインターを優先するケースは全体的にほとんどないと思います。生のポインターをいつ使用するかの推奨事項で見落としたり誤解したりする可能性があるため、結論に至るまでの資料を確認する必要があります。

于 2016-07-08T21:25:57.017 に答える