C ++ 17のいくつかの新機能により、よりコンパクトで明確なコードを記述できます。 これは、テンプレートのメタプログラミングでは特に重要であり、その結果はしばしば不気味に見えます...
たとえば、コンパイル時に計算されるif
を表現するif
、 SFINAEテクニック( enable_if
)または静的ディスパッチ(タグディスパッチ)を使用してコードを記述する必要があります。 これらの表現は理解するのが難しく、高度なメタプログラミングパターンに慣れていない開発者にとっては魔法のように見えます。
幸いなことに、C ++ 17の登場により、 if constexpr
得られます。 これでSFINAEと静的ディスパッチのほとんどのテクニックが不要になり、コードが削減され、「通常の」 if
ようになります。
この記事では、 if constexpr
いくつかの使用方法をif constexpr
ます。
はじめに
if constexpr
C ++ 17で導入されif constexpr
便利な機能if constexpr
、フォームのif constexpr
静的if
。 最近、Jensの記事の著者がif constexpr
を使用してコードを単純化する方法に関する会議がMeeting C ++サイトで公開されました。
新しい機能がどのように機能するかを示すことができるいくつかの追加の例を見つけました。
- 数値比較
- ファクター変数ファクトリー
これらの例が、C ++ 17からのstaticの理解に役立つことを願っています。
しかし、最初に、 enable_if
の基本を更新したいと思います。
コンパイル時に必要なのはなぜですか?
これについて初めて聞いたとき、なぜ静的if
とこれらの複雑なテンプレート式が必要なのif
と尋ねるかもしれません...正常なif
動作ではないでしょうか?
例を考えてみましょう:
template <typename T> std::string str(T t) { if (std::is_same_v<T, std::string>) // return t; else return std::to_string(t); }
この関数は、オブジェクトのテキスト表現を表示するためのシンプルなツールとして機能します。 to_string
はstd::string
型のパラメーターを受け入れないため、これを確認し、 t
がstringの場合は単にt
返します。 簡単そうに聞こえますが、このコードをコンパイルしてみましょう。
// , auto t = str("10"s);
次のようなものが得られます。
In instantiation of 'std::__cxx11::string str(T) [with T = std::__cxx11::basic_string<char>; std::__cxx11::string = std::__cxx11::basic_string<char>]': required from here error: no matching function for call to 'to_string(std::__cxx11::basic_string<char>&)' return std::to_string(t);
is_same
は使用された型(文字列)に対してtrue
を与え、変換せずにt
を返すことができます...
主な理由はこれです:コンパイラは両方の条件分岐を解析しようとし、 else
の場合にエラーを見つけました。 テンプレートを指定する特定のケースでは、「間違った」コードを拒否できません。
このため、コードを「除外」し、条件に一致するブロックのみをコンパイルするif
は、静的が必要です。
std :: enable_if
C ++ enable_if
で静的if
を記述する1つの方法は、 enable_if
(およびC ++ 14以降のenable_if
)を使用することenable_if
。 それはかなり奇妙な構文を持っています:
template< bool B, class T = void > struct enable_if;
enable_if
は、条件Bが真の場合にタイプTを推測します。 それ以外の場合、SFINAEによると、利用可能な関数オーバーロードから部分的な関数オーバーロードが削除されます。
次のように簡単な例を書き換えることができます。
template <typename T> std::enable_if_t<std::is_same_v<T, std::string>, std::string> str(T t) { return t; } template <typename T> std::enable_if_t<!std::is_same_v<T, std::string>, std::string> str(T t) { return std::to_string(t); }
簡単じゃないですか?
タイプが文字列である場合を区別するためにenable_if
を使用しenable_if
た...しかし、 enable_if
の使用を避けて、関数をオーバーロードするだけでまったく同じ効果を得ることができます。
次に、C ++ 17のif constexpr
してこのコードを簡素化します。 その後、 str
関数をすばやく書き直すことができstr
。
最初の使用-数値の比較
簡単な例から始めましょう。2つの数値で機能するclose_enough
関数です。 数値が浮動小数点でない場合(たとえば、2つの整数int
)、単純に比較できます。 浮動小数点数の場合、小さなイプシロン値を使用するのが最善です。
この例をPractical Modern C ++ Teaserで見つけました。これはPatrice RoyのモダンC ++の可能性の素晴らしい紹介です。 彼は親切に彼の例を含めることを許可してくれました。
C ++ 11/14のバージョン:
template <class T> constexpr T absolute(T arg) { return arg < 0 ? -arg : arg; } template <class T> constexpr enable_if_t<is_floating_point<T>::value, bool> close_enough(T a, T b) { return absolute(a - b) < static_cast<T>(0.000001); } template <class T> constexpr enable_if_t<!is_floating_point<T>::value, bool> close_enough(T a, T b) { return a == b; }
ご覧のとおり、ここでenable_if
使用されています。 これはstr
関数に非常に似ています。 コードは、 is_floating_point
タイプがis_floating_point
条件を満たすかどうかを確認します。 その後、コンパイラは関数のオーバーロードの1つを削除できます。
C ++ 17でこれがどのように行われるかを見てみましょう。
template <class T> constexpr T absolute(T arg) { return arg < 0 ? -arg : arg; } template <class T> constexpr auto precision_threshold = T(0.000001); template <class T> constexpr bool close_enough(T a, T b) { if constexpr (is_floating_point_v<T>) // << !! return absolute(a - b) < precision_threshold<T>; else return a == b; }
これは、基本的に通常の関数のように見える1つの関数です。 ほぼ「通常」のif
。
if constexpr
コンパイル時に評価され、式分岐のいずれかのコードがスキップされる場合。
C ++ 17のもう少し機能を使用します。 どれが見えますか?
2番目を使用する-可変数のパラメーターを持つファクトリー
Scott Myrs、C ++の効果的な使用の第18章では、 makeInvestment
と呼ばれるメソッドについて説明していmakeInvestment
。
template<typename... Ts> std::unique_ptr<Investment> makeInvestment(Ts&&... params);
これは、 Investment
クラスの相続人を作成するファクトリメソッドであり、最も重要なことは、さまざまな数のパラメータをサポートできることです!
たとえば、相続人のタイプは次のとおりです。
class Investment { public: virtual ~Investment() { } virtual void calcRisk() = 0; }; class Stock : public Investment { public: explicit Stock(const std::string&) { } void calcRisk() override { } }; class Bond : public Investment { public: explicit Bond(const std::string&, const std::string&, int) { } void calcRisk() override { } }; class RealEstate : public Investment { public: explicit RealEstate(const std::string&, double, int) { } void calcRisk() override { } };
この本の例は理想的すぎて機能していません-クラスのコンストラクターが同じ数と同じタイプの入力引数を受け入れる限り機能します。
スコット・マイレスは、彼の著書「Effective Use of C ++」の修正と追加で次のようにコメントしています。
makeInvestment
インターフェースmakeInvestment
実用的でmakeInvestment
ません。子孫は同じ引数のセットから作成できると想定されているためです。 これは、構築されたオブジェクトの選択の実装で特に顕著であり、引数は完全転送メカニズムを使用してすべてのクラスのコンストラクターに渡されます。
たとえば、2つのクラスがある場合、一方のコンストラクターは2つの引数を取り、残りの3つはこのコードをコンパイルしません。
// : Bond(int, int, int) { } Stock(double, double) { } make(args...) { if (bond) new Bond(args...); else if (stock) new Stock(args...) }
make(bond, 1, 2, 3)
を記述した場合、 else
下の式はコンパイルされないため、 Stock(1, 2, 3)
適したコンストラクターはありません! これが機能するためには、次のようなstatic if
なものが必要です。条件を満たした場合にのみコンパイルし、そうでない場合は破棄します。
動作する可能性のあるコードは次のとおりです。
template <typename... Ts> unique_ptr<Investment> makeInvestment(const string &name, Ts&&... params) { unique_ptr<Investment> pInv; if (name == "Stock") pInv = constructArgs<Stock, Ts...>(forward<Ts>(params)...); else if (name == "Bond") pInv = constructArgs<Bond, Ts...>(forward<Ts>(params)...); else if (name == "RealEstate") pInv = constructArgs<RealEstate, Ts...>(forward<Ts>(params)...); // pInv... return pInv; }
ご覧のとおり、「マジック」は、 constructArgs
関数内で発生します。
アイデアの背後にある理論的根拠は、 Type
指定された属性セットから構築される場合、 unique_ptr<Type>
を返すか、そうでない場合はnullptr
返すことです。
C ++ 17より前
この場合、 std::enable_if
ようにstd::enable_if
を使用します。
// C++17 template <typename Concrete, typename... Ts> enable_if_t<is_constructible<Concrete, Ts...>::value, unique_ptr<Concrete>> constructArgsOld(Ts&&... params) { return std::make_unique<Concrete>(forward<Ts>(params)...); } template <typename Concrete, typename... Ts> enable_if_t<!is_constructible<Concrete, Ts...>::value, unique_ptr<Concrete> > constructArgsOld(...) { return nullptr; }
std::is_constructible
使用std::is_constructible
と、特定の型が特定の引数のリストから構築されるかどうかをすばやく確認できstd::is_constructible
。 // @ cppreference.com
C ++ 17では少し簡単になり、新しいヘルパーが登場しました。
is_constructible_v = is_constructible<T, Args...>::value;
したがって、コードを少し短くすることができます...しかし、 enable_if
使用は依然としてひどく複雑です。 C ++ 17はどうですか?
if constexpr
更新されたバージョン:
template <typename Concrete, typename... Ts> unique_ptr<Concrete> constructArgs(Ts&&... params) { if constexpr (is_constructible_v<Concrete, Ts...>) return make_unique<Concrete>(forward<Ts>(params)...); else return nullptr; }
式の畳み込みを使用してアクションを記録することにより、機能を拡張することもできます。
template <typename Concrete, typename... Ts> std::unique_ptr<Concrete> constructArgs(Ts&&... params) { cout << __func__ << ": "; // : ((cout << params << ", "), ...); cout << "\n"; if constexpr (std::is_constructible_v<Concrete, Ts...>) return make_unique<Concrete>(forward<Ts>(params)...); else return nullptr; }
かっこいいね...
enable_if
を使用した式の複雑な構文はenable_if
なくなりました。 関数のオーバーロードも必要ありません。 1つの関数で表現力豊かなコードを書くことができます。
if constexpr
の式の条件の計算結果に応じて、コードの1ブロックのみがコンパイルされます。 この場合、オブジェクトが特定の属性セットから構築できる場合、 make_unique
呼び出しをコンパイルします。 そうでない場合は、 nullptr
を返します( make_unique
はコンパイルしません)。
おわりに
コンパイル時の条件式は、テンプレートの使用を大幅に簡素化する優れた機能です。 さらに、既存のソリューションである静的ディスパッチ(タグディスパッチ)またはenable_if
(SFINAE)を使用するよりもコードが明確になります。 これで、実行時のコードに「似た」意図を表現できます。
この記事では単純な表現のみを取り上げましたが、新機能の適用性をより広く検討することをお勧めします。
str
関数の例に戻ると、 if constexpr?
を使用して書き換えることができますif constexpr?