例を見てみましょう:
createArray(10, 20); // ? "10" ? "20" ? createArray(length=10, capacity=20); // , ! createArray(capacity=20, length=10); // .
架空の擬似言語の別の例:
window = new Window { xPosition = 10, yPosition = 20, width = 100, height = 50 };
このアプローチは、多数のオプションパラメータを持つ関数で特に便利です。呼び出されたときに、デフォルト値の一部のみを変更する必要があります。 一部のプログラミング言語は、名前付きパラメーター(C#、Objective-Cなど)をサポートしていますが、C ++はサポートしていません。 この投稿では、名前付きパラメーターをC ++でエミュレートするいくつかの古典的な方法を見て、新しいものを考えてみます。
コメント
偽物から始めましょう、しかし最も簡単な方法-コメントを通して名前付きパラメーターをエミュレートします:)
Window window { 10, // xPosition 20, // yPosition 100, // width 50 // height };
このアプローチは、Windows開発者の間で非常に人気があります。MSDNの例では、このようなコメントがよく提供されています。
「名前付きパラメーター」のイディオム
この考え方は 、Javaでのプログラミングスタイルに基づいています。つまり、すべてのオプションパラメータをメソッドの形式で含むプロキシクラスを作成します。 その後、これらのメソッドの呼び出しのチェーンを使用して、必要なパラメーターのみを設定できます。
// 1 File f { OpenFile{"path"} // .readonly() .createIfNotExist() . ... };
// 2 ( " -") File f = OpenFile { ... } .readonly() .createIfNotExist() ... ;
// 3 " -" - ( CreateFile) auto f = CreateFile ( OpenFile("path") .readonly() .createIfNotExists() . ... ));
OpenFileクラスはパラメーターのセットであり、Fileコンストラクターはこのクラスのオブジェクトを受け入れます。 一部の著者(たとえば、 ここ )は、OpenFileはプライベートメンバーのみを持ち、Fileクラスをフレンドリーにする必要があると主張しています。 パラメータを設定するためにより複雑なロジックを使用する場合、これは理にかなっています。 ただし、単純な値を割り当てるには、パブリックメソッドを使用した上記のスタイルで十分です。
このアプローチでは:
- 必須パラメーターは引き続き位置指定です(OpenFileコンストラクターの呼び出しは最初でなければならず、これは変更できません)
- オプションのパラメーターには、コピー(移動)コンストラクターが必要です
- 追加のプロキシクラスを記述する必要があります
「パラメーターのパッケージ」のイディオム
このアイデアは以前のアイデアに似ており、 Davide Di GennaroのAdvanced C ++ Metaprogramming book-プロキシオブジェクトを使用して代入演算子(=)を介してパラメーターを設定する技術から得られたため、次の構文シュガーを取得します。
MyFunction(begin(v), end(v), where[logger=clog][comparator=greater<int>()]);
関連するエンティティ:
- ロガーとコンパレーターはグローバル定数です。 割り当て演算子は、割り当てられた値のラップされたコピーを返すだけです
- ここで、「パラメータのパッケージ」タイプのグローバル定数です。 その演算子[]は、新しいプロキシオブジェクトを返すだけで、そのメンバーの1つを新しい引数に置き換えます。
文字内:
where = {a, b, c } where[logger = x] → { a,b,c }[ argument<0>(x) ] → {x,b,c}
実装の概要:
// argument template <size_t CODE, typename T = void> struct argument { T arg; argument(const T& that) : arg(that) { } }; // void argument - just to use operator= template <size_t CODE> struct argument<CODE, void> { argument(int = 0) { } template <typename T> argument<CODE, T> operator=(const T& that) const { return that; } argument<CODE, std::ostream&> operator=(std::ostream& that) const { return that; } }; // " " ( ) template <typename T1, typename T2, typename T3> struct argument_pack { T1 first; T2 second; T3 third; argument_pack(int = 0) { } argument_pack(T1 a1, T2 a2, T3 a3) : first(a1), second(a2), third(a3) { } template <typename T> argument_pack<T, T2, T3> operator[](const argument<0, T>& x) const { return argument_pack<T, T2, T3>(x.arg, second, third); } template <typename T> argument_pack<T1, T, T3> operator[](const argument<1, T>& x) const { return argument_pack<T1, T, T3>(first, x.arg, third); } template <typename T> argument_pack<T1, T2, T> operator[](const argument<2, T>& x) const { return argument_pack<T1, T2, T>(first, second, x.arg); } }; enum { LESS, LOGGER }; const argument<LESS> comparator = 0; const argument<LOGGER> logger = 0; typedef argument_pack<basic_comparator, less<int>, std::ostream> pack_t; static const pack_t where(basic_comparator(), less<int>(), std::cout);
完全なコードについては、元の本をご覧ください。
この手法は興味深いように見えますが、実際には、適度に快適で一般的なものにすることは困難です。 本の中で、それは一般に、我々が考えている問題を解決することによってではなく、演算子[]への「連鎖」呼び出しの例によって示されました。
タグ
AndrzejKrzemieńskiは興味深い投稿「直感的なインターフェイス」を公開し、次のことを提案しました。名前付きパラメーターは、コンパニオンのペア-実際の値と空の構造です(目的のオーバーロード関数を選択するには、異なるタイプの空の構造が必要です)。 STLからのこのアプローチの例を次に示します。
std::function<void()> f{std::allocator_arg, a}; // a - std::unique_lock<std::mutex> l{m, std::defer_lock}; // lock
Andrzejは一般化されたアプローチを提案しました:
// STL std::vector<int> v1(std::with_size, 10, std::with_value, 6);
理解できるように、オーバーロードされた関数をいくつか作成する必要があり、パラメーターの順序を選択することもできません。 プラスには、コピー/転送コンストラクターの必要性の欠如が含まれます。 デフォルトを渡すことも問題なく機能します。 記事から:「タグは、呼び出されるいくつかの場所でのみ役立つオーバーロード関数で名前空間を詰まらせるため、理想的なソリューションではありません」
さらに、ある読者は別のタグ実装の良いアイデアを提案しました:
std ::ベクトルv1(std :: with_size(10)、std :: with_value(6));
ブースト
Boostにはパラメーターライブラリがあります 。
ご想像のとおり、これはかなり完全で実用的な実装です。 例:
// #include <boost/parameter/name.hpp> #include <boost/parameter/preprocessor.hpp> #include <string> BOOST_PARAMETER_NAME(foo) BOOST_PARAMETER_NAME(bar) BOOST_PARAMETER_NAME(baz) BOOST_PARAMETER_NAME(bonk) BOOST_PARAMETER_FUNCTION( (int), // function_with_named_parameters, // tag, // "". BOOST_PARAMETER_NAME, "tag" (required // (foo, (int)) (bar, (float)) ) (optional // , - (baz, (bool) , false) (bonk, (std::string), "default value") ) ) { if (baz && (bar > 1.0)) return foo; return bonk.size(); } // function_with_named_parameters(1, 10.0); function_with_named_parameters(7, _bar = 3.14); function_with_named_parameters( _bar = 0.0, _foo = 42); function_with_named_parameters( _bar = 2.5, _bonk= "Hello", _foo = 9); function_with_named_parameters(9, 2.5, true, "Hello");
最新のC ++の名前付きパラメーター
最新のC ++言語標準は、新しい扉を開きます。 それらのいずれかを適用して問題を解決できるかどうかを見てみましょう。
ラムダス
連鎖方法は冗長すぎます。 オブジェクト自体を返す一連の関数を追加したくありません。 ラムダ関数を使用して構造を決定し、そのメンバーを設定するのはどうですか?
struct FileRecipe { string Path; // bool ReadOnly = true; // bool CreateIfNotExist = false; // // ... }; class File { File(string _path, bool _readOnly, bool _createIfNotexist) : path(move(_path)), readOnly(_readOnly), createIfNotExist(_createIfNotExist) {} private: string path; bool readOnly; bool createIfNotExist; }; auto file = CreateFile( "path", [](auto& r) { // - - r.CreateIfNotExist = true; });
パラメータを保存するためのクラスが必要ですが、アプローチ自体は、すべての「チェーン」関数を明示的に登録する必要がある名前付きパラメータの古典的なイディオムよりも優れています。 別のオプションは、FileRecipe型のオブジェクトを受け入れるFileクラスのコンストラクターを作成することです。
必須パラメーターの読みやすさを改善するにはどうすればよいですか? このアプローチとタグを組み合わせてみましょう:
auto file = CreateFile( _path, "path", [](auto& r) { r.CreateIfNotExist = true; });
確かに、彼らはまだ定位置です。 ランタイムに「必要なパラメーターがありません」というエラーが表示される可能性がある場合は、 オプションのタイプを使用できます
私は最近、このアプローチを使用してテストとモックを構成しようとしました。 たとえば、単純なサイコロゲームのテストを作成する必要がありました。 構成とテストは次のようになります。
TEST_F(SomeDiceGameConfig, JustTwoTurnsGame) { GameConfiguration gameConfig { 5u, 6, 2u }; }
このアプローチを使用すると、次のようになります。
TEST_F(SomeDiceGameConfig, JustTwoTurnsGame) { auto gameConfig = CreateGameConfig( [](auto& r) { r.NumberOfDice = 5u; r.MaxDiceValue = 6; r.NumberOfTurns = 2u; }); }
マクロを使用して、同じラムダが呼び出された各テストで繰り返されないようにすることもできます。
TEST_F(SomeDiceGameConfig, JustTwoTurnsGame) { auto gameConfig = CREATE_CONFIG( r.NumberOfDice = 5u; r.MaxDiceValue = 6; r.NumberOfTurns = 2u; ); }
可変長テンプレートの使用
C ++ 11で導入された可変長テンプレートは、上記の方法を改善できます。 もう一度タグを思い出しましょう。 タグは、ラムダ+パラメータオブジェクトよりも優れたアプローチになります。別のオブジェクトを作成する必要がないため、コピーコンストラクタには問題がなく、すべてのパラメータは同じ方法で処理されます(ラムダでは、必要なパラメータを別々に処理する必要がありました)。 ただし、タグは次の場合にのみ十分なアプローチになります。
- オーバーロードされたコンストラクターまたは関数を1つだけ宣言して取得する
- パラメーターの順序を自由に決定する機会を得ます(「タグ値」のペア)
- 必須パラメーターとオプションパラメーターの両方があります
次のようなもの:
File f { _readonly, true, _path, "some path" };
または:
File f { by_name, Args&&... args) {}
私の考えは次のとおりです。Variadicテンプレートを使用して、ユーザーにパラメーターの順序を決定し、オプションのパラメーターを省略できるようにします。
2つのコンストラクターを想像してください。
File(string path, bool readonly, bool createIfNotExist) {} // template<typename... Args> File(by_name_t, Args&&... args) {}
File型のオブジェクトは、2つの方法のいずれかで作成できます。 2番目のコンストラクターを使用する場合、セット内のすべてのパラメーターを調べて、対応するパラメーターセットを使用して最初のコンストラクターを呼び出します。 パラメータの表示とコードの生成はコンパイル段階で実行されます。これは線形の時間を要し、ランタイムでの呼び出しに費やされる時間には影響しません。
この実装は単なるスケッチであり、確実に改善することができます。
クラスの設計方法は次のとおりです。
File(string path, bool readonly, bool createIfNotExists /*...*/) : _path (move(path)), _createIfNotExist(createIfNotExist), _readonly(readonly) // ,etc... { } template<typename Args...> File(named_tag, Args&&... args) : File{ REQUIRED(path), OPTIONAL(read, false) // , etc... } // { }
動作するコードを示す前に、同じアイデアをプロキシに適用できることを明確にしましょう。
auto f = File { by_name, readonly=true, path="path" };
ここでの主な違いは、引数を渡すことにあります。プロキシでは、構文糖(演算子=)を取得しますが、値を格納して渡す必要があります(移動不可能/コピー型にはあまり適していません)。
ここで、コードを試すことができます。 タグ付きバージョンから始めてからプロキシに切り替えたため、両方のバージョンがあります。 「PACK UTILS」と呼ばれる2つのセクションがあります(タグとプロキシ用)。
クラスは次のようになります。
class window { public: // window( string pTitle, int pH, int pW, int pPosx, int pPosy, int& pHandle) : title(move(pTitle)), h(pH), w(pW), posx(pPosx), posy(pPosy), handle(pHandle) { } // , (_title = "title") template<typename... pack> window(use_named_t, pack&&... _pack) : window { REQUIRED_NAME(title), // required OPTIONAL_NAME(h, 100), // optional OPTIONAL_NAME(w, 400), // optional OPTIONAL_NAME(posx, 0), // optional OPTIONAL_NAME(posy, 0), // optional REQUIRED_NAME(handle) } // required { } // , (__title, "title") template<typename... pack> window(use_tags_t, pack&&... _pack) : window { REQUIRED_TAG(title), // required OPTIONAL_TAG(h, 100), // optional OPTIONAL_TAG(w, 400), // optional OPTIONAL_TAG(posx, 0), // optional OPTIONAL_TAG(posy, 0), // optional REQUIRED_TAG(handle) } // required { } private: string title; int h, w; int posx, posy; int& handle; };
ご覧のとおり、両方の最後のコンストラクターは常に「クラシック」コンストラクターを呼び出して実際の作業を行います。
次のコードは、ユーザーがオブジェクトを作成する方法を示しています。
int i=5; // window w1 {use_tags, __title, "Title", __h, 10, __w, 100, __handle, i}; cout << w1 << endl; // window w2 {use_named, _h = 10, _title = "Title", _handle = i, _w = 100}; cout << w2 << endl; // window w3 {"Title", 10, 400, 0, 0, i}; cout << w3 << endl;
長所:
- 必須およびオプションのパラメーターは均一に使用されます。
- 順序は厳密に定義されていません
- タグ付きメソッドには、パラメーターの受け渡しに関連する欠点はありません
- プロキシを使用したメソッドは非常に明白です(演算子=のため)
短所:
- コンパイル段階でのエラーは理解しにくい場合があります(static_assertが役立つ場合があります)
- 利用可能なパラメータを文書化する必要があります。
- 不要な関数/コンストラクターによる名前空間の「汚染」
- デフォルト値は常に計算されます。
- タグ付きメソッドは、可視性の点で完全ではありません(タグと値はコンマに従います)
- プロキシメソッドは、パラメーターを渡すという点では理想的ではありません。
最初の問題に注意してください:Clangは問題を非常に明確に報告するのに十分スマートです。 ウィンドウ名の必須パラメーターを忘れてしまったことを想像してください。コンパイラーの出力は次のとおりです。
main.cpp:28:2: error: static_assert failed "Required parameter" static_assert(pos >= 0, "Required parameter"); ^ ~~~~~~~~ main.cpp:217:14: note: in instantiation of template class 'get_at<-1, 0>' requested here : window { REQUIRED_NAME(title), ^
これで、何がどこで見逃されたかを正確に知ることができます。
std :: tupleを使用したミニマルなアプローチ
[この段落はDavide Di Gennaroによって書かれました]
タプル機能(std :: tuple)を使用して、タスクの非常にコンパクトで移植可能な実装を作成できます。 いくつかの簡単な原則に依存します。
- パラメータのセットは特別なタプルになり、各「タグタイプ」の後に値が移動します(つまり、タイプは(std :: tuple <age_tag、int、name_tag、string、...>)のようになります)
- 標準言語ライブラリには、オブジェクトとタプルを転送/連結するための機能がすでに含まれており、パフォーマンスと正確性が保証されています
- マクロを使用して、タグを表すグローバル定数を定義します。
- パラメータセットを作成するための構文は、(tag1 = value1)+(tag2 = value2)+ ...
- クライアントは、テンプレートタイプへの参照としてパラメーターセットを受け入れます。
テンプレート<typename pack_t>
void MyFunction([whatever]、T&parameter_pack)//またはconst T&、T &&など
- 関数呼び出し内で、クライアントはパラメーターのセットから必要な値を抽出し、それらを何らかの方法で使用します(たとえば、ローカル変数に書き込みます)。
namespace tag { CREATE_TAG(age, int); CREATE_TAG(name, std::string); } template <typename pack_t> void MyFunction(T& parameter_pack) { int myage; std::string myname; bool b1 = extract_from_pack(tag::name, myname, parameter_pack); bool b2 = extract_from_pack(tag::age, myage, parameter_pack); assert(b1 && myname == "John"); assert(b2 && myage == 18); } int main() { auto pack = (tag::age=18)+(tag::name="John"); MyFunction(pack); }
このアイデアの実装は次のようになります。
最初のマクロ:
#include <tuple> #include <utility> template <typename T> struct parameter {}; #define CREATE_TAG(name, TYPE) \ \ struct name##_t \ { \ std::tuple<parameter<name##_t>, TYPE> operator=(TYPE&& x) const \ { return std::forward_as_tuple(parameter<name##_t>(), x); } \ \ name##_t(int) {} \ }; \ \ const name##_t name = 0
マクロCREATE_TAG(age、int)を展開すると、クラスとグローバルオブジェクトが作成されます。
struct age_t { std::tuple<parameter<age_t>, int> operator=(int&& x) const { return std::forward_as_tuple(parameter<age_t>(), x); } age_t(int) {} }; const age_t age = 0;
概念的に割り当て
age = 18
次のようなものに変換します。
make_tuple(parameter<age_t>(), 18);
私たちが書いたことに注意してください:
std::tuple<parameter<age_t>, int> operator=(int&& x) const
右側にr値が必要です。 これはセキュリティのために行われます。パラメータセットを使用してコードを読みやすくするために、変数ではなく定数を割り当てることができます。
int myage = 18; f(myage); // ok g((...) + (age=18)); // ok g((...) + (age=myage)); // ,
さらに、移動のセマンティクスを使用できます。
の違い
std::tuple<parameter<age_t>, int> operator=(int&& x) const { return std::make_tuple(parameter<age_t>(), x); }
そして
std::tuple<parameter<age_t>, int> operator=(int&& x) const { return std::forward_as_tuple(parameter<age_t>(), x); }
とても薄い。 後者の場合、std :: tuple <...、int &&>が返されますが、関数がstd :: tuple <...、int>を返すため、移動コンストラクタstd :: tupleが呼び出されます。
別の方法として、次のように書くことができます。
std::tuple<parameter<age_t>, int> operator=(int&& x) const { return std::make_tuple(parameter<age_t>(), std::move(x)); }
そして、タプルに適した連結演算子を作成します。
パラメータで始まるすべてのタプルがコードによって作成されたことに暗黙的に同意するため、明示的な検証を行わずに、単にパラメータを破棄します。
template <typename TAG1, typename... P1, typename TAG2, typename... P2> std::tuple<parameter<TAG1>, P1..., parameter<TAG2>, P2...> operator+ (std::tuple<parameter<TAG1>, P1...>&& pack1, std::tuple<parameter<TAG2>, P2...>&& pack2) { return std::tuple_cat(pack1, pack2); }
非常に単純な機能:両方のタプルが次の形式であることを確認します
tuple<parameter<tag>, type, [maybe something else]>
それらを接続します。
そして最後に、セットから引数を抽出する関数を作成します。 この関数には転送セマンティクスがあることに注意してください(つまり、呼び出し後、パラメーターはセットから抽出されます)。
template <typename TAG, typename T, typename... P, typename TAG1> bool extract_from_pack(TAG tag, T& var, std::tuple<parameter<TAG1>, P...>& pack);
次のように機能します。セットにパラメーターが含まれている場合、変数はその直後の値を受け取り、関数はtrueを返します。 そうしないと、何か悪いことが起こります(コンパイルエラーを選択し、falseを返し、例外をスローできます)。
この選択を可能にするため、関数は次のようになります。
template <typename ERR, typename TAG, typename T, typename... P, typename TAG1> bool extract_from_pack(TAG tag, T& var, std::tuple<parameter<TAG1>, P...>& pack)
次のように呼び出します。
extract_from_pack< erorr_policy > (age, myage, mypack);
可変長テンプレートを使用するための規則を考慮して、extract_from_packはパラメーターセットがタプル<パラメーター、...>の形式であることを認識しているため、TAGがTAG1と等しいかどうかを再帰的に確認する必要があります。 クラスを呼び出すことでこれを実装します。
extract_from_pack< erorr_policy > (age, myage, mypack);
原因
extractor<0, erorr_policy >::extract (age, myage, mypack);
さらに原因
extractor<0, erorr_policy >::extract (age, myage, std::get<0>(pack), mypack);
次の2つのオーバーロードオプションがあります。
extract(TAG, … , TAG, …)
実行された場合、割り当てを実行し、trueまたは
extract(TAG, … , DIFFERENT_TAG, …)
繰り返しを続けて、再度呼び出します
extractor<2, erorr_policy >::extract (age, myage, mypack);
反復を継続できない場合、error_policy :: err(...)が呼び出されます
template <size_t N, typename ERR> struct extractor { template <typename USERTAG, typename T, typename TAG, typename... P> static bool extract(USERTAG tag, T& var, std::tuple<parameter<TAG>, P...>&& pack) { return extract(tag, var, std::get<N>(pack), std::move(pack)); } template <typename USERTAG, typename T, typename TAG, typename... P> static bool extract(USERTAG tag, T& var, parameter<TAG> p0, std::tuple<P...>&& pack) { return extractor<(N+2 >= sizeof...(P)) ? size_t(-1) : N+2, ERR>::extract(tag, var, std::move(pack)); } template <typename USERTAG, typename T, typename... P> static bool extract(USERTAG tag, T& var, parameter<USERTAG>, std::tuple<P...>&& pack) { var = std::move(std::get<N+1>(pack)); return true; } }; template <typename ERR> struct extractor<size_t(-1), ERR> { template <typename TAG, typename T, typename DIFFERENT_TAG, typename... P> static bool extract(TAG tag, T& var, std::tuple<parameter<DIFFERENT_TAG>, P...>&& pack) { return ERR::err(tag); } }; template <typename ERR, typename TAG, typename T, typename... P, typename TAG1> bool extract_from_pack(TAG tag, T& var, std::tuple<parameter<TAG1>, P...>& pack) { return extractor<0, ERR>::extract(tag, var, std::move(pack)); }
パラメーターセットの柔軟な性質により、「falseを返す」を最良のエラー処理ポリシーと見なすことができます(実際には、より厳密な動作は各パラメーターが必須であることを意味します)。
struct soft_error { template <typename T> static bool err(T) { return false; } };
それでも、何らかの理由で必要な場合は、次の2つから選択できます。
struct hard_error { template <typename T> static bool err(T); // , static_assert(false) . ? }; struct throw_exception { template <typename T> static bool err(T) { throw T(); return false; } };
追加の改善は、次のような場合の冗長性テストです。
(age=18)+(age=19)
最終ノート
次のようなランタイム手法については説明しませんでした。
void MyFunction (option_parser& pack) { auto name = pack.require("name").as<string>(); auto age = pack.optional("age", []{ return 10; }).as<int>(); ... }
コードは実行時に動作し、作業中に必要なパラメーターを取得しようとするため、時間の無駄があります。まあ、エラーが発生した場合にのみエラーを知ることができます。 コードは理想からはほど遠いです。「概念の証明」としてのみ引用していますが、この形式では実際のプロジェクトで使用できるとは思いません。
また、 ここで C ++言語標準に名前付きパラメーターを追加する提案も見つけました。 いいですね。