C ++のリンクの何が問題になっています



免責事項:現時点では、C ++ 11の操作経験が十分ではないため、すべての考慮事項はC ++ 03のコンテキストでのみ検討する必要があります。



C ++の参照は、演算子オーバーロードメカニズムの構文上のニーズを満たすように見えました。 純粋なCには参照型はありませんが、その代わりに、「代入演算子の左側に何ができるか」というあいまいな表現で説明される左辺値の概念があります。



//  C int a; int foo(int); a = 7; //  a - int 5 = 7; //   5 - int foo(42) = 7; //   foo(42) -  int
      
      





この小さな例では、変数a、リテラル「5」、および関数呼び出しfoo(42)の3つの式は同じ型(int)ですが、変数のみが左辺値であり、代入演算子の左側にあります。



Cプログラマーの観点からは、式「foo(42)= 7;」は常識を欠いており、コンパイルすべきではありませんが、演算子のオーバーロードの出現により、そのような式の必要性が生じました。







C ++では、配列要素にアクセスする操作は、メンバー関数演算子[](size_t n)の呼び出しとして扱われます。 そして、代入演算子の左側に立つことができるものを返す必要があります。 そして、これを説明できるタイプが必要です。 だからリンクがありました。



ポインタのようなリンクは、オブジェクトのアドレスをメモリに保存しますが、ポインタによって構文的に逆参照されます。 これにより、上記の問題を解決できますが、新しい問題が発生します。



言語構文では、ターゲットオブジェクトとリンク自体を区別できません。リンクに対するすべての操作は、実際にはオブジェクトに対する操作です。 これの結果として:

1.リンクを別のオブジェクトに再割り当てすることはできません。

2.リンクに含まれるアドレスを別のオブジェクトのアドレスまたはNULLと比較することはできません。



これらのプロパティから、他の制限が順番に続きます。

3.リンクは作成時に初期化する必要があります(後で初期化することはできないため)。

4.リンクにnullアドレスを含めることはできません(検証および処理が不可能であるため)。



最後の2つのプロパティは、リンクの大きな利点です。 これらの2つのプロパティのために、リンクを優先してポインターを放棄する推奨事項によく出くわします(たとえば、 このコーディングガイド別の1つStackOverflowの説明、そして悲しいことにあなたの謙虚な使用人が現在作業しているコーディングガイド)。



ただし、リンクの構文とセマンティクスの矛盾により多くの問題が発生するため、反対の意見があります(たとえば、 GoogleTrolltechのエンジニアはポインターを好む)。



リンクを使用して出力引数を関数に渡すと、関数呼び出しを読み取るときに「出力」の事実が非常にわかりにくくなります。

  color.getHsv(&h, &s, &v); //      getHsv()    h,s,v color.getHsv(h, s, v); //   h,s,v   
      
      





定数参照の使用は、値によるオブジェクトの最適化された転送の事実上の標準になりました。 「const SomeClass&arg」というエントリを見て、この場合、参照がSomeClassクラスのインスタンスに変更権なしで渡されるという事実を考えます。関数がこのインスタンスで動作することが重要です。 ここでは、SomeClass型の値が渡されると思います。 そして、値が渡されたら、この値を含むこのクラスのオブジェクトをこの関数に渡すことができます。



リンクはメタプログラミングでいくつかの困難を引き起こし、 Boost.Refのような松葉杖を生成します。



リンクをSTLコンテナの要素にすることはできません。 リンクフィールドを持つクラスの場合、(ダーティハックに頼らずに)代入演算子を実装することはできません。 したがって、そのようなクラスのオブジェクトもコンテナの要素にはなりません。



最近検出されたバグに基づく:

 template<class T> T foo(T x) { ... } template<class T> class Bar { public: static T baz(T x) { return foo(x); } }; std::string str = Bar<std::string>::baz(getTitle()); //  ColorDescriptor& desc = Bar<ColorDescriptor&>::baz(getColorDescriptor()); // !
      
      





別の興味深い例を次に示します。

 template<class T> class SizeOfTest { public: static bool sizeOfIsOK() { return sizeof(SizeOfTest<T>) >= sizeof(T); } private: T m_data; }; struct BigData { char d[1000]; }; assert(SizeOfTest<int>::sizeOfIsOK()); //  assert(SizeOfTest<BigData>::sizeOfIsOK()); //  assert(SizeOfTest<BigData&>::sizeOfIsOK()); // !
      
      





したがって、リンクはC ++のポインターの完全な代替として機能することはできません。 このためではなく、作成されました。



しかし、一方で、「クリーン」なポインターの需要は目に見えます-型システムがそれらがNULLではなく初期化されることを保証するポインター。 そして、最も興味深いのは、プロパティ(3,4)がその性質上、ポインターのセマンティクスと競合しないことです。 この問題は、C ++で使用可能なツールの限られた選択によってのみ発生します。



少し夢見て、後方互換性のフレームワークから自由になりましょう。



私の意志であれば、プロパティ(3,4)をポインタ自体のプロパティにして、セマンティクスを保持します。 T. e。

 int a = 5, b = 5; int* p1; //  int* p2 = null; //  int* p3 = &a; int* p4 = &b; assert(p3 != p4); assert(*p3 == *p4); p3 = &b; assert(p3 == p4); int * p5 = std::min(p3, p4); int * p6 = new int(5); // new    ,    if (p5) { ... } //  -   bool      .
      
      





しかし、NULLはどうでしょうか? 結局のところ、オプションのセマンティクスが依然として必要な場合があります。 NULL可能ポインターに戻す代わりに、ポインターのセマンティクスに対してオプションの直交を実装することにより、より良い結果を得ることができます。

 int a = 5; int? b = 5; //   int int? c = null; //   int assert(a == b); assert(b != c); int* p0 = &a; int*? p1 = &a; int*? p2 = null; int*? p3 = &b; //  int?* p4 = &b; int?*? p5 = null; p5 = p4; p4 = p5; //  *p0 = 7; *p1 = 7; // : p1 -    if(p1 != null) { *?p1 = 7; } p0 = ?p1;
      
      





リンクなしで行うことは可能ですか? やってみましょう。



定数リンクで引数を渡すことから始めましょう。 このような送信方法は、引数を値で渡す最適化された方法です。 一部のタイプでは、この最適化は理にかなっていますが、他のタイプではそうではありません。



この最適化に関して適切な決定を下すには、以下を考慮する必要があります。



プログラマーは、各機能の各パラメーターについてこれらのパラメーターをすべて詳細に分析することはできません-これは非常に時間のかかるタスクです。 さらに、結果はターゲットハードウェアプラットフォームごとに異なります。 そのため、決定をコンパイラに割り当て、コードの作成時からコンパイル時に決定を延期することをお勧めします。



このアプローチには欠点があります-コンパイルと関数へのポインターを別々に実装するには、コンパイラーは関数の実装に隠された要因を考慮せずに決定を下す必要があります。 しかし、これらの制限にもかかわらず、コンパイラによる最適化は手動よりも悪くないと思います。



コンストラクタのコピーはどうですか? 通常の関数に対して「この引数に対してコピーコンストラクターを呼び出すことができる」というセマンティクスが適切な場合、無限再帰が許可されるため、コピーコンストラクター引数としては受け入れられません。 この問題は、少なくとも2つの方法で解決できます。

  1. コピーコンストラクターに明示的に例外を追加します-コンパイラーは常に参照による転送を選択します。

     class MyClass { public: MyClass(MyClass src) //    const MyClass& src. { ... } };
          
          



  2. 引数をポインターでコピーコンストラクターに渡し、何らかの方法でそれを修飾します。

     class MyClass { public: MyClass(const MyClass* src, std::copy_ctor_tag) { ... } };
          
          





オペレーターのオーバーロードに戻ります。



純粋なCでは、限られた一連のステートメントのみが左辺値を返すことができます。配列アクセス、さまざまな割り当てタイプ、プレフィックスのインクリメントとデクリメント、およびデリファレンス自体です。 それだけです これらの演算子では、関数へのマッピング方法を変更して、ポインターを返すことができます。

 a[i] = b; *a.operator[](i) = b; (++i) = x; *i.operator++() = x; (x = y) = z; *x.operator=(y) = z; *p = d; *p.operator->() = d;
      
      





同時に、逆参照演算子は非ロードになります-代わりに、演算子->がすべての作業を行います。



他のすべての場合、左辺値を使用する可能性は、最も驚きの原則と矛盾します-式「a + b」が引数の1つを変更するコードをデバッグしたり、レコード「foo(42)= 7がレビュー中に意味する」を理解したりしないでください; "。



ルールの例外はI / Oフローです。 ストリーム自体を引数として<<演算子に渡すことはできません-値で渡されます。 そのため、ストリームオブジェクトを参照し、同時に値によって安全に送信できるものを渡す必要があります。 これは、ストリームへのポインタ、またはより適切な特別なラッパーオブジェクトにすることができます。

 int main() { std::fstream filestr("test.txt", fstream::out); std::outref(&filestr) << "foo = " << foo << ", bar = " << bar << std::endl; return 0; } std::outref operator<<(std::outref ref, MyClass obj) { ref << obj.x; ref << obj.y; ref << obj.z; return ref; }
      
      





何も見逃していなかった場合、C ++でリンクなしで実行できる可能性が非常に高いです。



まとめ



今日、安全なポインタの必要性を満たすためにリンクを使用する傾向があります。 リンクはその構文特性により、このニーズを非常に不十分に満たしています。 定数参照は、値による引数の受け渡しを最適化するために使用されますが、この最適化の責任はコンパイラに移されます。 リンクが解決しようとしている元の問題は、他の方法で解決できます。 参照は、C ++の非常に疑わしい取得であり、ポインターセマンティクスを使用した安全なポインターの方がはるかに価値があります。



All Articles