12

ほとんどの人がそれを悪い習慣と考えていることは知っていますが、クラスのパブリックインターフェイスを参照でのみ機能させ、ポインターを内部に保持し、必要な場合にのみ機能させようとしている場合、探している値を伝える何かを返す方法はないと思いますコンテナには存在しません。

クラスリスト {
    公衆:
        値 &get(タイプ キー);
};

クラスのパブリック インターフェイスで危険なポインターが見られるのを避けたいと考えてみましょう。この場合、例外をスローして not found を返すにはどうすればよいでしょうか。

それに対するあなたのアプローチは何ですか?空の値を返し、その空の状態をチェックしますか? 私は実際には throw アプローチを使用していますが、チェック方法を紹介します。

クラスリスト {
   公衆:
      bool exists(タイプキー);
      値 &get(タイプ キー);
};

したがって、最初に値が存在することを確認するのを忘れると、例外が発生します。これは実際には例外です。

どのようにしますか?

4

11 に答える 11

16

STL は、反復子を使用してこの状況に対処します。たとえば、std::map クラスには同様の関数があります。

iterator find( const key_type& key );

キーが見つからない場合は、'end()' を返します。この反復子アプローチを使用するか、戻り値にある種のラッパーを使用することができます。

于 2008-09-27T15:12:10.043 に答える
6

(Alexandrescu によると) 正解は次のとおりです。

Optional強制する

まず第一に、アクセサーを使用してください。ただし、車輪を発明することなく、より安全な方法で:

boost::optional<X> get_X_if_possible();

次に、enforceヘルパーを作成します。

template <class T, class E>
T& enforce(boost::optional<T>& opt, E e = std::runtime_error("enforce failed"))
{
    if(!opt)
    {
        throw e;
    }

    return *opt;
}

// and an overload for T const &

このように、値がないことが何を意味するかに応じて、明示的にチェックします。

if(boost::optional<X> maybe_x = get_X_if_possible())
{
    X& x = *maybe_x;

    // use x
}
else
{
    oops("Hey, we got no x again!");
}

または暗黙的に:

X& x = enforce(get_X_if_possible());

// use x

効率を重視する場合、または障害が発生した場所で適切に処理したい場合は、最初の方法を使用します。2 番目の方法は、他のすべての場合です。

于 2008-09-27T21:16:01.793 に答える
5

presents()の問題は、存在するものを2回検索することになります(最初にそこにあるかどうかを確認してから、もう一度見つけます)。これは非効率的です。特に(「リスト」の名前が示すように)コンテナが検索がO(n)である場合は非効率的です。

確かに、二重検索を回避するために内部キャッシュを実行することはできますが、実装が乱雑になり、クラスが一般的でなくなり(特定のケースに最適化されているため)、おそらく例外セーフまたはスレッドではなくなります-安全な。

于 2008-09-27T15:43:59.577 に答える
5

このような場合は例外を使用しないでください。C ++には、例外がスローされない場合でも、そのような例外に対して重要なパフォーマンスオーバーヘッドがあり、さらに、コードに関する推論がはるかに困難になります(例外の安全性を参照)。

C ++のベストプラクティスは、次の2つの方法のいずれかです。どちらもSTLで使用されます。

  • マーティンが指摘したように、イテレータを返します。実際、イテレータはtypedef単純なポインタの場合もあり、それに反対するものは何もありません。実際、これはSTLと一致しているため、この方法が参照を返すよりも優れていると主張することもできます。
  • を返しますstd::pair<bool, yourvalue>。ただし、これにより、値を変更することはできなくなります。これは、のコピーコンpairが呼び出され、国民投票のメンバーでは機能しないためです。

/編集:

この答えはかなりの論争を引き起こしました。コメントからは見えますが、多くの反対票からはそれほど見えません。私はこれがかなり驚くべきことに気づきました。

この答えは、最終的な参照点として意図されたものではありません。「正しい」答えはすでにマーティンによって与えられていました:例外はこの場合の振る舞いをかなりよく反映していません。例外以外のシグナリングメカニズムを使用する方が意味的に意味があります。

罰金。私はこの見解を完全に支持します。もう一度言及する必要はありません。代わりに、私は答えに追加の側面を与えたかった。マイナーなスピードブーストは、意思決定の最初の理由になることはありませんが、それ以上の議論を提供することができ、場合によっては、それらが重要になることさえあります。

実際、パフォーマンスと例外安全性という2つの側面について説明しました。後者はかなり議論の余地がないと思います。強力な例外保証を与えることは非常に困難ですが(もちろん、最強は「nothrow」です)、それは不可欠だと思います。例外をスローしないことが保証されているコードは、プログラム全体の推論を容易にします。多くのC++専門家はこれを強調しています(たとえば、「効果的なC++」の項目29のScottMeyers)。

スピードについて。Martin Yorkは、これは現代のコンパイラにはもはや当てはまらないと指摘しています。私は敬意を表して反対します。C ++言語では、環境が実行時に、例外の場合に巻き戻される可能性のあるコードパスを追跡する必要があります。さて、このオーバーヘッドは実際にはそれほど大きくはありません(そしてこれを確認するのは非常に簡単です)。上記のテキストの「自明ではない」は強すぎた可能性があります。

ただし、C ++のような言語と、C#のような多くの最新の「管理された」言語を区別することが重要だと思います。スタックを巻き戻すために必要な情報がとにかく保持されるため、例外がスローされない限り、後者には追加のオーバーヘッドはありません。概して、私の選択した言葉を支持してください。

于 2008-09-27T15:21:04.377 に答える
2

アクセサー?

私の前に提案された「イテレータ」のアイデアは興味深いものですが、イテレータの本当のポイントはコンテナを介したナビゲーションです。単純なアクセサーとしてではありません。

私はpaercebalに同意します、イテレータは反復することです。私はSTLのやり方が好きではありません。しかし、アクセサーのアイデアはもっと魅力的なようです。では、何が必要ですか?テスト用のブール値のように感じますが、元のリターンタイプのように動作するクラスのようなコンテナ。これはキャスト演算子で実現可能です。

テンプレート<T>クラスアクセサー{
    公衆:
        Accessor():_ value(NULL)
        {}

        アクセサー(T&value):_ value(&value)
        {}

        演算子T&()const
        {{
            if(!_value)
               throw Exception( "これは問題であり、どこかで間違いを犯しました。");
            そうしないと
               *_valueを返します。
        }

        演算子bool()const
        {{
            return _value!= NULL;
        }

    プライベート:
        T * _value;
};

さて、予見可能な問題はありますか?使用例:

アクセサー<type>value= list.get(key);

if(value){
   タイプ&v=値;

   v.doSomething();
}
于 2008-09-27T20:37:35.583 に答える
2

STLイテレータ?

私の前に提案された「イテレーター」のアイデアは興味深いものですが、イテレーターの真の目的はコンテナー内を移動することです。単純なアクセサとしてではありません。

アクセサーが数ある中の 1 つである場合は、イテレーターを使用してコンテナー内を移動できるため、イテレーターが適しています。しかし、アクセサーが単純なゲッターであり、値または値がないという事実を返すことができる場合、イテレーターはおそらく美化されたポインターにすぎません...

それは私たちを導く...

スマートポインター?

スマート ポインターのポイントは、ポインターの所有権を単純化することです。共有ポインターを使用すると、オーバーヘッドを犠牲にして共有されるリソース (メモリ) を取得できます (共有ポインターは参照カウンターとして整数を割り当てる必要があります...)。

選択する必要があります: Value が既に共有ポインター内にある場合、この共有ポインター (または弱いポインター) を返すことができます。またはあなたの値は生のポインタの中にあります。その後、行ポインタを返すことができます。リソースがまだ共有ポインター内にない場合は、共有ポインターを返したくありません。共有ポインターが範囲外になり、通知せずに値を削除すると、面白いことが起こります...

:-p

ポインタ?

インターフェイスがそのリソースの所有権について明確であり、返される値が NULL になる可能性があるという事実によって、単純な生のポインターを返すことができます。コードのユーザーが、オブジェクトのインターフェイス コントラクトを無視したり、ポインタを使って算術演算を行ったりするほど愚かな場合、値を返すために選択する他の方法を破るほど愚かになります。知的障害者は気にしないでください...

未定義の値

Value 型が実際に何らかの「未定義」の値を持っていて、ユーザーがそれを知っていて、それを処理することを受け入れる場合を除き、ポインターまたはイテレーターのソリューションと同様に、それは可能な解決策です。

しかし、あなたが尋ねた問題のために、「未定義」の値を Value クラスに追加しないでください。「参照対ポインター」戦争を別のレベルの狂気に引き上げることになります。コードのユーザーは、与えられたオブジェクトが正常であること、または存在しないことを望んでいます。このオブジェクトがまだ有効であるコードを 1 行おきにテストする必要があるのは苦痛であり、ユーザー コードを無駄に複雑にしてしまいます。

例外

例外は通常、一部の人々が望んでいるほどコストがかかりません。ただし、単純なアクセサーの場合、アクセサーが頻繁に使用される場合、コストは簡単ではありません。

たとえば、STL std::vector には、インデックスを介してその値への 2 つのアクセサーがあります。

T & std::vector::operator[]( /* index */ )

と:

T & std::vector::at( /* index */ )

違いは、[]投げないことです。そのため、ベクトルの範囲外にアクセスすると、おそらくメモリの破損や遅かれ早かれクラッシュする危険性があります。したがって、それを使用してコードを検証したことを本当に確認する必要があります。

一方、投げatています。これは、ベクトルの範囲外にアクセスすると、クリーンな例外が発生することを意味します。エラーの処理を別のコードに委任したい場合は、この方法が適しています。

[]ループ内の値にアクセスするときなどに個人的に使用します。私atは、例外が現在のコード (または呼び出し元のコード) を返す良い方法であると感じたときに、何かがうまくいかなかったという事実を使用します。

だから何?

あなたの場合、次を選択する必要があります。

非常に高速なアクセスが本当に必要な場合は、スローするアクセサーが問題になる可能性があります。しかし、これは、コードで既にプロファイラーを使用して、これがボトルネックであると判断したことを意味しますね。

;-)

値がないことが頻繁に発生する可能性があることがわかっている場合、および/またはクライアントに、アクセスされた値への可能なnull/無効/その他のセマンティックポインターを伝播させたい場合は、ポインターを返します(値が単純なポインター内にある場合)または弱い/共有ポインター (値が共有ポインターによって所有されている場合)。

ただし、クライアントがこの "null" 値を伝達しない、またはコード内で NULL ポインター (またはスマート ポインター) を伝達すべきではないと思われる場合は、例外によって保護された参照を使用します。ブール値を返す「hasValue」メソッドを追加し、値がない場合でもユーザーが値を取得しようとした場合にスローを追加します。

最後になりましたが、オブジェクトのユーザーが使用するコードを検討してください。

// If you want your user to have this kind of code, then choose either
// pointer or smart pointer solution
void doSomething(MyClass & p_oMyClass)
{
   MyValue * pValue = p_oMyClass.getValue() ;
   
   if(pValue != NULL)
   {
      // Etc.
   }
}

MyValue * doSomethingElseAndReturnValue(MyClass & p_oMyClass)
{
   MyValue * pValue = p_oMyClass.getValue() ;
   
   if(pValue != NULL)
   {
      // Etc.
   }

   return pValue ;
}

// ==========================================================

// If you want your user to have this kind of code, then choose the
// throwing reference solution
void doSomething(MyClass & p_oMyClass)
{
   if(p_oMyClass.hasValue())
   {
      MyValue & oValue = p_oMyClass.getValue() ;
   }
}

したがって、主な問題が上記の 2 つのユーザー コードのどちらかを選択することである場合、問題はパフォーマンスではなく、「コードのエルゴノミー」です。したがって、パフォーマンスの問題が発生する可能性があるため、例外ソリューションを脇に置くべきではありません。

:-)

于 2008-09-27T19:02:06.827 に答える
1

結果としてshared_ptrを返すのはどうですか。アイテムが見つからなかった場合、これはnullになる可能性があります。ポインタのように機能しますが、オブジェクトを解放します。

于 2008-09-27T15:19:16.797 に答える
1

(これが常に正しい答えであるとは限らず、私の口調は少し強いと思いますが、他のより複雑な代替案を決定する前に、この質問を検討する必要があります):

では、ポインタを返すことの何が問題になっていますか?

私はこれをSQLで何度も見ました。そこでは、伝染性の病気などがあるように、人々はNULL列を決して処理しないように真剣に取り組んでいます。代わりに、彼らは巧妙に-1、9999のような「空白」または「存在しない」人工的な値、あるいは「@X-EMPTY-X@」のようなものを思いつきます。

私の答え:言語にはすでに「そこにない」ための構造があります。どうぞ、それを使用することを恐れないでください。

于 2008-09-28T05:04:24.473 に答える
0

@aradtke、あなたは言った。

私はpaercebalに同意します。イテレータは反復することです。私はSTLのやり方が好きではありません。しかし、アクセサーのアイデアはもっと魅力的です。それで、何が必要ですか?テスト用のブール値のように見えますが、元の戻り値の型のように動作するコンテナのようなクラス。それはキャスト演算子で実現可能です。[..] さて、予見可能な問題はありますか?

まず、OPERATOR bool は必要ありません。詳細については、 Safe Bool イディオムを参照してください。しかし、あなたの質問について...

ここに問題があります。ユーザーはケースでキャストを明示する必要があります。ポインターのようなプロキシ(イテレーター、ref-counted-ptrs、生のポインターなど) には、簡潔な「get」構文があります。呼び出し元が追加のコードで呼び出す必要がある場合、変換演算子を提供してもあまり役に立ちません。

例のような参照から始めて、それを書くための最も簡潔な方法:

// 'reference' style, check before use
if (Accessor<type> value = list.get(key)) {
   type &v = value;
   v.doSomething();
}
// or
if (Accessor<type> value = list.get(key)) {
   static_cast<type&>(value).doSomething();
}

これで問題ありません。誤解しないでください。必要以上に冗長です。何らかの理由で list.get が成功することがわかっているかどうかを考えてみましょう。それで:

// 'reference' style, skip check 
type &v = list.get(key);
v.doSomething();
// or
static_cast<type&>(list.get(key)).doSomething();

ここで、イテレータ/ポインタの動作に戻りましょう:

// 'pointer' style, check before use
if (Accessor<type> value = list.get(key)) {
   value->doSomething();
}

// 'pointer' style, skip check 
list.get(key)->doSomething();

どちらもかなり優れていますが、ポインター/イテレーターの構文は少し短くなっています。「参照」スタイルにメンバー関数「get()」を与えることができます...しかし、それはすでにoperator*()とoperator->()の目的です。

「ポインター」スタイルのアクセサーには、演算子「unspecified bool」、operator*、および operator-> が含まれるようになりました。

そして何を推測しますか... raw ポインターがこれらの要件を満たしているため、プロトタイプの場合、list.get() は Accessor の代わりに T* を返します。次に、リストの設計が安定したら、戻ってきて、ポインターのようなプロキシ型であるアクセサーを作成できます。

于 2008-09-27T21:33:48.910 に答える
0

このような状況で私が好むのは、「get」をスローすることです。パフォーマンスの問題や失敗が一般的な状況では、「bool tryGet(type key, value **pp)」の行に沿って「tryGet」関数を使用します。 true が返された場合 *pp == 何らかのオブジェクトへの有効なポインタ、それ以外の場合 *pp は null です。

于 2008-09-27T15:02:26.620 に答える
-6

興味深い質問です。私が推測する参照を排他的に使用するのは C++ の問題です。Java では、参照はより柔軟で、null になる可能性があります。null参照を強制するのが合法的なC++かどうか思い出せません:

MyType *pObj = nullptr;
return *pObj

しかし、私はこれを危険だと考えています。ここでも Java では例外をスローしますが、これは一般的な例外ですが、C++ で自由に例外が使用されることはめったにありません。再利用可能な C++ コンポーネント用のパブリック API を作成していて、参照を返す必要がある場合は、例外ルートに行くと思います。私の本当の好みは、API がポインターを返すようにすることです。私はポインターを C++ の不可欠な部分と考えています。

于 2008-09-27T15:01:20.397 に答える