C ++ 17のif constexprによるコードの簡素化

C ++ 17のいくつかの新機能により、よりコンパクトで明確なコードを記述できます。 これは、テンプレートのメタプログラミングでは特に重要であり、その結果はしばしば不気味に見えます...


たとえば、コンパイル時に計算されるifを表現するifSFINAEテクニック( 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_stringstd::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?



Source: https://habr.com/ru/post/J351970/


All Articles