void foo(T&out) - 出力パラメータを修正する方法

関数から値を返す必要があるが、戻り値を使用できない場合があります。たとえば、一度に複数の値を返したい関数で発生します。関数に複数の入力を渡すことはできますが、同じ方法で複数の戻り値を渡すことはできません。

C++ プログラマーは、そのために古き良き (左辺値) 参照を使用する傾向があります。 パラメーターとして参照し、その参照に出力を割り当てます。呼び出し元は変数を渡し、関数の完了時に変更された変数の値を見つけます。

ただし、このアプローチにはいくつかの問題があります。まず、呼び出しを見ただけでは、変数が変更されることは明らかではありません。これが、Google が使用するものなどの C++ スタイル ガイドがそのためのポインターの使用を推奨する理由です。 .呼び出し元は、変数のアドレスを明示的に渡して、明示的にする必要があります。

しかし、ポインターを使用して 18 で渡すことができるようになりました 、関数でそれを確認する必要があります:あなたが本当に「参照」を意味するポインターは、私が提唱してきたガイドラインに従っていません。

では、普遍的な解決策はないのでしょうか?

ありますが、まず問題の全容を理解する必要があります.

モチベーション

更新:免責事項

この記事は、一般的な出力パラメータの使用に賛成または反対する意図はありません。動機は単に、出力パラメータが人々が使用したいと思うものであることを認めることがここにあるということです。

出力パラメータを使用するように説得したくありません しない 出力パラメーターを使用してください。出力パラメーターを使用する場合は、ここで説明する手法を使用することを検討してください。エラーが発生しにくいためです。ただし、出力パラメーターを使用したくない場合は、使用しないでください。

続きを読む

関数 27 があると仮定しましょう 複数の値を返したい場合:

int func(int in_a, int in_b, int& out);

左辺値参照を使用すると、次のように呼び出すことができます:

int output;
auto result = func(42, 11, output);

ただし、既に述べたように、これにはいくつかの問題があります。

    <リ>

    31 であることは明らかではありません は変更されます。

    <リ>

    40 最初に作成する必要があります - これにはデフォルトのコンストラクターが必要です!この例では、まったく初期化されていません。関数が値を設定せずに戻ると、これは問題になる可能性があります (実装のバグ、または文書化された動作のため)

しかし、関数の定義で発生する別の問題があります。ストリームからすべての文字列を連結する次の関数を考えてみましょう。次の定義は、驚くべき結果につながる可能性があります:

bool read_strings(std::istream& in, std::string& out)
{
 for (std::string tmp; in >> tmp;)
 out += tmp;
 return !out.empty();
}

55 69 を連結します 7385 を繰り返し呼び出すことによって .これは、94 の場合にのみ望ましい結果が得られます 最初は空でした。次の呼び出し

std::string str = "abc";
read_strings(in, str);

106 の値を保持します 117 で .これは驚くべき動作かもしれません。

したがって、実装する場合でも 「素朴な」出力パラメータを持つ関数は、注意して誤って読み取らないようにする必要があります 既知の値に設定する前にそれから.これは、呼び出し元のすべての問題に加えて.

解決策は?

120 を使用するだけで、これらすべてを回避できます。 :

struct func_result
{
 int first_value;
 int second_value;
};

func_result func(int a, int b);

…

auto res = func(42, 11);
std::cout << res.first_value << ' ' << res.second_value << '\n';

実際のコードでは、すべてに適切な名前を使用します。 ここに示されていますが、要点はわかります。新しい type を宣言しました 戻り値の型については、2 つの値を表し、格納できる型です。その後、その型を一度に返すことができます。

134 を使用する」というだけのブログ投稿は書きません。 複数の値を返したい場合」.これは C++ コア ガイドラインでも推奨されています.さらに、それは常に解決策ではありません:

std::string a, b;
std::cin >> a >> b;

あなたは今何をするつもりですか?!

関数の戻り値の型を使用できない状況はたくさんあります。演算子のオーバーロードは最も説得力のないものです。何かに渡すコールバックやその他の形式のファンクタを使用して、コールバックすることもできます。

いずれの場合も、戻り値の型は固定されているため、144 は使用できません。 .

解決策

Google スタイル ガイドと出力パラメータに関する特定のルールについての議論で、冗談めかして誰かの話を聞きました。 - 156 を使用することを提案 .わからない場合は、168 177 を返します 、これは reference のラッパーです 割り当てが参照するオブジェクトを変更するポインター。暗黙的に 182 に変換可能 、したがって、元の例で次のように使用できます:

int output;
auto result = func(42, 11, std::ref(output));

しかし、コンパイラはそれを強制しないので、それほど優れたものではありません.次のステップは、パラメータを変更することかもしれません. 出力パラメータを 195 に変更するとどうなりますか ?

int func(int in_a, int in_b, std::reference_wrapper<int> out);

209 であるため、これは良い考えではありません。 参照のようには動作しません。割り当ては参照を再バインドするため、213 を使用する必要があります 227 の代わりに .さらに、235 まだです 参照から暗黙的に作成されるため、呼び出し元に明白にならずに渡すことができます。

しかし、パラメータの型を変更することは正しい方向への一歩です。必要なのは新しいだけです タイプ - 243 .この型には 259 が必要です 261 を取るコンストラクタ オブジェクトへのポインターを格納します。次に、271 を取る代入演算子が必要です。 そしてそれをポインタに割り当てます:

template <typename T>
class output_parameter
{
public:
 explicit output_parameter(T& obj)
 : ptr_(&obj) {}

 // disable assignment because it would have totally different semantics then the other operator=
 // also it is really not needed
 output_parameter& operator=(const output_parameter&) = delete;

 output_parameter& operator=(T value)
 {
 *ptr_ = std::move(value);
 return *this;
 }

private:
 T* ptr_;
};

これをパラメーターの型にすると、呼び出し元は次のように記述する必要があります:

int output;
auto result = func(42, 11, output_parameter<int>(output));

うーん、それもかもしれません verbose.問題ありません。単純にヘルパー関数を使用してください:

int output;
auto result = func(42, 11, out(output));

281 であることは明らかです は出力パラメーターであり、呼び出しから変更されます。さらに、292 を渡すことはできません 直接、コンパイラによって強制されます

  • 最初の不利な点が解消されました

309 を見てみましょう 再び実装:

bool read_strings(std::istream& in, output_parameter<std::string> out)
{
 std::string result;
 for (std::string tmp; in >> tmp;)
 result += tmp;
 out = std::move(result);
 return !result.empty();
}

318 は使えないので 329 で 、一時的な文字列を使用して移動する必要があります:335 から誤って読み取ることはできません .しかし、この実装にはバグがあります - 347 の後 、 357 空かもしれません。そのため、最初に結果を取得する必要があります:

bool read_strings(std::istream& in, output_parameter<std::string> out)
{
 std::string result;
 for (std::string tmp; in >> tmp;)
 result += tmp;
 auto empty = result.empty();
 out = std::move(result);
 return !empty;
}

確かに、それは冗長です。

360 からの読み取りを防止したい 値を知る前に 371 を追加すると 関数など、これは静的にチェックされません。

どうすればそれができますか?

簡単:代入演算子の戻り値の型を変更するだけです。382 です 慣例により、390 を許可する .しかし、代入演算子は通常の代入演算子のように実際には動作しないため、その規則を変更しても害はありません。したがって、戻り値の type を変更できます。 :唯一の欠点は、405 を実行できないことです。 、しかし、とにかくセマンティクスは何でしょうか?

では、418 の署名を変更しましょう :

T& operator=(T value)
{
 *ptr_ = std::move(value);
 return *ptr_;
}

戻り値の型を 426 に変更しました 値を返すようにします。これがまさに私たちが望んでいることです:値を取得できますが、それが既知の状態にあることがわかった後でのみ!取得する方法はありません 割り当てた後にしか取得できないため、割り当てずに値を取得します!

433 の実装 次のようになります:

bool read_strings(std::istream& in, output_parameter<std::string> out)
{
 std::string result;
 for (std::string tmp; in >> tmp;)
 result += tmp;
 return !(out = std::move(result)).empty();
}

440 と呼びます 出力型の値である代入演算子の結果!

しかし、今度は 2 つの文字列を作成する必要があり、移動割り当てのコストがかかります。改善できますか?

もちろん、実装を変更するだけです:

bool read_strings(std::istream& in, output_parameter<std::string> out)
{
 auto& result = (out = "");
 for (std::string tmp; in >> tmp;)
 result += tmp;
 return !result.empty();
}

450 を割り当てます 空の文字列に直接変換し、出力パラメーターを操作できるようにします。このクラスだけで、以前に発生したバグを完全に排除しました:

std::string str = "abc";
read_strings(in, out(str));

型設計により、このバグはもう発生しません !

このようにして、問題のうち 2 つを解決しました。残っているのは、デフォルトのコンストラクターの要件だけです。

デフォルト以外の構築可能な型の許可

関数呼び出しの前に、出力として使用される変数を作成する必要があります。これには、デフォルトのコンストラクター、または少なくとも値を事前に初期化する何らかの方法が必要です。必要なのは、ストレージを作成する方法です。 オブジェクト自体ではなく、オブジェクトのために.まだそこにないかもしれないオブジェクトを表す必要があります.

464 だと思ったら または - より良い - 470 オプションは、値を持つか、値を持たない型です。はい、デフォルトのコンストラクターを必要とせず、481 オプションを処理できるようにします。

しかし、これは私たちが望む抽象化ではありません。

有効期間全体を通じて変数に null 状態を導入したくはありません。必要なのは、初期化が遅延され、初期化できるまで延期される変数です。しかし、重要な点は次のとおりです。 /em> 初期化され、そのまま 初期化されたので、再度初期化を解除することはできません

  • これではコードが不必要に複雑になるだけです。

答えは省略可能なインターフェイスで、493 です。 .オプションのように、505 があります 初期化されているかどうかを照会する関数と 519 値を返します。しかし、基本的な違いは次のとおりです。一度 522 539 を返します 、する オブジェクトの存続期間全体にわたって true を返すため、安全に信頼できます。

547 を使用して実装できます そのように:

template <typename T>
class deferred_construction
{
public:
 deferred_construction() = default; // creates it un-initialized

 deferred_construction(const deferred_construction&) = default;
 deferred_construction(deferred_construction&&) = default;

 ~deferred_construction() = default;

 // see below
 deferred_construction& operator=(const deferred_construction&) = delete;

 // initializes it
 deferred_construction& operator=(T value)
 {
 assert(!has_value()); // see below
 opt_ = std::move(value);
 return *this;
 }

 // + variadic emplace(Args&&... args) to initialize in-place

 bool has_value() const
 {
 return opt_.has_value();
 }

 // + non-const overload
 const T& value() const
 {
 return opt_.value();
 }

private:
 type_safe::optional<T> opt_;
};

実装は簡単で、通常とは異なる設計上の決定が 2 つあるだけです。

まず、代入演算子がありません。これは、初期化を解除できないようにするために必要です。それ以外の場合は、書き込みが許可されます:

deferred_construction<T> obj;
obj = T(…);
obj = deferred_construction<T>();

単純にその割り当てをノーオペレーションにするか、552 をアサートすることができますが、 566 の場合に値を持ちます 値があるので、それを削除するというより抜本的なアプローチを選択しました。

次に 576 オブジェクトを初期化するには、オブジェクトがまだ初期化されていない必要があります。オプション自体はそれを処理できますが、私はそれを防ぐことにしました。理由は簡単です。値が初期化されると、581 ラッパーは役に立ちません。本来すべきことを行っています。その後、 598 を使用できます (使用する必要があります)。

これで、 601 を簡単に拡張できます 619 も受け入れることができるように 出力パラメータの最初の代入は遅延構築オブジェクトの代入を使用する必要がありますが、初期化されている場合は 629 を使用する必要があります 割り当てます。

次に、次のように記述できます。

deferred_construction<std::string> output;
read_strings(in, out(output));

そして、このコードは最初の実装とまったく同じように動作します。より安全で明白であり、デフォルトのコンストラクターを必要としません。

結論

634 誤って値を読み取ることができず、呼び出しが明らかな「より良い」出力パラメーターを許可します。 649 と組み合わせる デフォルトで構築できない型の出力パラメータを許可します。

おそらくご想像のとおり、すべてのより洗練された実装は、私の type_safe ライブラリにあります。