最も正確で安全なprintf

カットの下で、私は非常に怒っていて、ユーザーリテラル(新しい標準から)をより良く知る方法についての魅力的な物語を見つけるでしょうが、同時に後で上記の関数を実装し、 constexprを見つけて 、後で非常に同じリテラルをリハビリしました。



物語



2009年までさかのぼって、今後のユーザーリテラルに関する神話がインターネットに登場しました。これにより、コンパイル時に文字列を解析するなど、すべてを完全に行うことができます。 (ちなみに、 食欲を イカルニツキーに感謝します-読む前に見ることをお勧めします。)これは、テンプレートでのオプションを指します。 しかし、そこにはありませんでした。 パターンを使用した実装は、デジタルリテラルに対してのみ許可されます。 そして、これは、この方法でコンパイル中に数値のみを解析できることを意味します。



プロット。 ソリューションへの最初のステップ



それから私は動揺しました。 しかし、Googleでは、コンパイル時にテンプレートを使用せずに文字列を解析できることがわかりました。



したがって、安全なprintf問題の解決策の一部です。

template< class > struct FormatSupportedType; #define SUPPORTED_TYPE(C, T) \ template<> struct FormatSupportedType< T > { \ constexpr static bool supports(char c) { return c == C; } } SUPPORTED_TYPE('c', char); SUPPORTED_TYPE('d', int); template< std::size_t N > constexpr bool checkFormatHelper(const char (&format)[N], std::size_t current) { return current >= N ? true : format[current] != '%' ? checkFormatHelper( format, current + 1 ) : format[current + 1] == '%' ? checkFormatHelper( format, current + 2 ) : false; } template< std::size_t N, class T, class... Ts > constexpr bool checkFormatHelper(const char (&format)[N], std::size_t current, const T& arg, const Ts & ... args) { return current >= N ? false : format[current] != '%' ? checkFormatHelper( format, current + 1, arg, args... ) : (format[current] == '%' && format[current + 1] == '%') ? checkFormatHelper( format, current + 2, arg, args... ) : FormatSupportedType< T >::supports(format[current + 1]) && checkFormatHelper( format, current + 2, args... ); } template< std::size_t N, class... Ts > constexpr bool checkFormat(const char (&format)[N], const Ts & ... args) { return checkFormatHelper( format, 0, args... ); } int main() { static_assert( checkFormat("%c %d\n", 'v', 1), "Format is incorrect" ); }
      
      





作業のロジックは非常に明確だと思います。可能なオプションを並べ替え、タイプと文字の関係に応じて結果を返します。 追加のクラスを使用して、サポートが存在し、シンボルと一致することを確認します。 その結果、コンパイル中にフォーマットの正確性を検証できます(もちろんコンパイル段階で既知の場合)。 次に、従来のprintfを使用して、結果を印刷します。

 template< std::size_t N, class... ARGS > int safe_printf(const char (&format)[N], ARGS... args) { static_assert( checkFormat(format, args... ), "Format is incorrect" ); return printf( format, args... ); }
      
      





しかし、私のgcc-4.7はこれを食べたくありません! 私は突然再び動揺することを決めたが、洞察が来た。 先に進むには、 constexprを理解する必要があります。 以下、記事の最も興味深い部分だと思います。



クライマックス。 constexprを理解する



前に何が起こったのですか? 以前はコンパイル段階と実行段階がありましたが、タイピングがコンパイル段階で発生することにも注意する必要があります(誰もがこれを知っていますが)。

今何がありますか? コンパイル段階で関数を実行できるconstexprがあります-ある種のしゃれが出てきます。 明確な定義を導入する必要があります:コンパイルと実行だけでなく、プログラムの特定の部分のコンパイルと実行(この場合は、コンパイル中にオブジェクトを使用することもできるため、関数)を検討します。 たとえば、「関数fのコンパイル」、「関数fの実行時」、「プロジェクト全体のコンパイル」、「プロジェクトの実行時」。

つまり、プロジェクト全体のコンパイル段階が、プロジェクトのさまざまなユニットのコンパイルと実行に分割されました。 例を見てみましょう。

 template< int N > constexpr int f(int n) { return N + n; } int main() { constexpr int i0 = 1; constexpr int i1 = f<i0>(i0); constexpr int i2 = f<i1>(i1); static_assert(i2 == 4, ""); }
      
      





すぐにそれをコンパイルするが、有用な何もしないと言わなければならないmain()関数のコンパイルを詳しく見てみましょう。 まず、変数i0に割り当てられ、次にこの変数を使用して変数i1の値が計算されますが、計算するには関数f <i0>(i0)を実行する必要がありますが、これにはコンパイルする必要があり、コンパイルには値i0が必要です。 同様にf <i1>(i1)で 。 つまり、次のようになります: main()関数をコンパイルするプロセスには、関数f <1>(int)の順次コンパイル、その実行、次に関数f <2>(int)のコンパイル、それに応じてその実行が含まれます。

なに? constexprとして指定された関数は、最も一般的な関数のように動作します。 関数fを見てみましょう。Nはコンパイルの段階で知られており、 nは実行の段階で知られています。



デノウメント。 Secure Printfの実装



それがコンパイルされたくない理由です!

 template< class... ARGS > int safe_printf(const char* format, ARGS... args) { static_assert( checkFormat(format, args... ), "Format is incorrect"); return printf( format, args... ); }
      
      





static_assertは safe_printf関数のコンパイル段階で許可され、 フォーマットはその実行時にのみ認識されます (この段階でコンパイル段階が他のもののためであっても)。

そして、どうやってこれを回避しますか? 方法はありません、またはコンパイル段階で見えるようにテンプレート文字にフォーマット文字を挿入します(そして、私たちが覚えているように、カスタムリテラルの使用はこれを可能にしません)、またはすべてが非常にクールで強力で無敵のC + +(およびC ++ 11でも)無力になり、マクロが登場します!

 #define safe_printf(FORMAT, ...) \ static_assert(checkFormat( FORMAT, __VA_ARGS__ ), "Format is incorrect"); \ printf(FORMAT, __VA_ARGS__) int main() { safe_printf("%c %d\n", 'v', 1); }
      
      





勝利!



Denouement-何が実際に起こったのか、押してはいけないのか



いつものように、最初にハッピーエンドを表示し、次にそれがどのようになったかを示します。 以下は、安全なprintfの正しい実装です。

 template< char... > struct TemplateLiteral { }; template< char... FORMAT, class... ARGS > int safe_printf_2(TemplateLiteral<FORMAT...>, ARGS... args) { constexpr char format[] = {FORMAT... , '\0'}; static_assert( checkFormat(format, args... ), "Format is incorrect"); return printf( format, args... ); } int main() { safe_printf_2(_("%c %d\n"), 'v', 2); }
      
      





これらの関数は、そのTYPEが興味深い(そして値ではない)変数と、推論する必要のある引数に渡されます。 リテラルをテンプレートに変換するメカニズムを実装することは残っています。 理想的には、リテラルが存在するコンテキストで、このリテラル(列挙型のようなもの) インデックスのパックが残っていて、それをアンパックできるようにすれば、クールです。

 template< std::size_t... INDXs > //... TemplateLiteral<"some literal"[INDXs]...> //...
      
      





ただし、リテラルの長さとパックの長さは一致する必要があり、 パックは外部にのみ入力できるため、リテラルは外部に渡す必要があり、外部に渡す場合(ただし、パラメーターとしてテンプレートに入れるメカニズムはまだありません)、単純に送信されますテンプレートは型であり、型はコンパイルであるため、関数の引数であり、したがって、テンプレートでラップされる関数をコンパイルする段階では不明です。要するに、これは不可能です。

しかし、マクロについてもう一度覚えておいてください。 boost ::プリプロセッサに数値のリスト生成するように依頼できます。 もちろん、その数は静的であり、前処理の段階でのみ変更できます。 また、コンパイル段階でリテラルから要素をインデックスで取得することを制御する必要があるため、何らかの保護メカニズムを提供する必要があります。また、行末をクリーンにする必要があります。 また、何らかの方法ですべての文字列がキャプチャされているかどうかを確認する必要があります。 プログラマーは長すぎるリテラルを入力しましたか? 以下はコードです。

 template< char... > struct TemplateLiteral { }; //       ; //      ,    template< std::size_t LEN, char CHAR, char... CHARS > struct TemplateLiteralTrim { private: //      //   - ,       template< bool, class, char... > struct Helper; template< char... C1, char... C2 > struct Helper< false, TemplateLiteral<C1...>, C2... > { //  , static_assert(sizeof...(C1) == LEN, "Literal is too large"); typedef TemplateLiteral<C1...> Result; }; template< char... C1, char c1, char c2, char... C2 > struct Helper< true, TemplateLiteral<C1...>, c1, c2, C2... > { typedef typename Helper< (bool)c2, TemplateLiteral<C1..., c1>, c2, C2...>::Result Result; }; public: typedef typename Helper<(bool)CHAR, TemplateLiteral<>, CHAR, CHARS..., '\0' >::Result Result; }; template< class T, std::size_t N > constexpr inline std::size_t sizeof_literal( const T (&)[N] ) { return N; } //      N-   template< std::size_t M > constexpr inline char getNthCharSpec( std::size_t N, const char (&literal)[M] ) { return N < M ? literal[N] : '\0'; } #define GET_Nth_CHAR_FOR_PP(I, N, LIT) ,getNthCharSpec(N, LIT) //      //      , // -      , //    #define TEMPLATE_LITERAL_BASE(MAX, LIT) \ (typename TemplateLiteralTrim< sizeof_literal(LIT) - 1 \ BOOST_PP_REPEAT(MAX, GET_Nth_CHAR_FOR_PP, LIT) >::Result()) // MAX_SYM         #define TEMPLATE_LITERAL(LITERAL) TEMPLATE_LITERAL_BASE(MAX_SYM, LITERAL) int main() { //  safe_printf_2(TEMPLATE_LITERAL("%c %d\n"), 'v', 2); }
      
      





ちなみに、 boost ::プリプロセッサを調べるのは非常に面白かったです-何ができるのか想像できませんでした(算術演算など)。 したがって、マクロは本当にひどい力です。



未リリースのフレーム。 ユーザーリテラルのリハビリテーション



それにもかかわらず私が彼ら(文字)を尊重し始めた理由を示す時が来ました。 むかしむかし、約2年前、タプルについて学びました。 それらは私にとって非常に便利に思えましたが、これらのタプルはPython、Nemerl、Haskellからのものでした。 そして、C ++からタプルについて学んだとき、私はstd :: get <N>(タプル)に非常に腹を立てていました-面倒だと思ったので、それから要素を受け取るためのメカニズムを開発したかったのですが、角括弧演算子を使っていました そして、ここでユーザーリテラルが助けになりました。

 template< std::size_t > struct Number2Type { }; template< class... Ts > class tupless: public std::tuple<Ts...> { public: template< class... ARGS > tupless(ARGS... args): std::tuple<Ts...>(args...) { } template< std::size_t N > auto operator[](Number2Type<N>) const -> decltype(std::get<N>(std::tuple<Ts...>())) const& { return std::get<N>(*this); } template< std::size_t N > auto operator[](Number2Type<N>) -> decltype(std::get<N>(std::tuple<Ts...>())) & { return std::get<N>(*this); } }; template< std::size_t N > constexpr std::size_t chars_to_int(const char (&array)[N], std::size_t current = 0, std::size_t acc = 0) { return (current >= N || array[current] == 0) ? acc : chars_to_int(array, current + 1, 10 * acc + array[current] - '0'); }; template<char... Cs> constexpr auto operator "" _t() -> Number2Type<chars_to_int((const char[1 + sizeof...(Cs)]){Cs..., '\0'})> { return {}; //      }; int main() { tupless<char, int, float> t('x', 10, 12.45); safe_printf_2(TEMPLATE_LITERAL("%c %d %f"), t[0_t], t[1_t], t[2_t]); }
      
      





何がそんなに面白いの? まあ、最初に、リテラルを返すタイプを2回書かないようにするため(つまり、タイプは私たちにとって興味深い)、空の初期化リストが使用され、コンパイラはそれを目的のタイプのオブジェクトにキャストし、そこにコンストラクターを挿入しようとします。

このユーザーリテラルは、そのタイプが値に直接依存するという点で非常に興味深いものです。 たとえば、リテラルタイプ2_tNumber2Type <2>になります。 だから、ここで、みんなが快適になることを願っています

もちろん、これを標準ライブラリに追加するといいでしょう...



更新:マクロの代わりに関数を使用することをお勧めします



更新:トピックを「異常なプログラミング」に移動しました。ここで彼にとってより快適になると思います。



All Articles