yohhoyの日記

技術的メモをしていきたい日記

std::views::splitで文字列分割

C++2a(C++20) Rangesライブラリstd::views::splitを利用した文字列分割処理。*1
2021-06-24追記:次期C++2b(C++23)標準ライブラリではよりシンプルな実装が可能となる。

split2str関数は「char範囲を区切り文字delimで分割しstd::stringのViewへ変換するレンジアダプタ(range adaptor)」を返す。

// C++2a(C++20)
#include <iomanip>
#include <iostream>
#include <string>
#include <string_view>
#include <ranges>

auto split2str(char delim) {
  return std::views::split(std::views::single(delim))
       | std::views::transform([](auto v) {
           auto cv = v | std::views::common;
           return std::string{cv.begin(), cv.end()};
         });
}

int main() {
  std::string_view s = "apple,banana,cinnamon";
  for (const auto& word : s | split2str(',')) {
    std::cout << std::quoted(word) << "\n";
  }
}
// "apple"
// "banana"
// "cinnamon"

ノート:std::views::splitは区切り要素(char)ではなく “区切りパターン(range)” を引数にとるため、std::views::single(delim)のように単一要素レンジを明示する必要がある雰囲気。splitに左辺値delimを指定すると意図通りに動作せず、キャストによりchar型右辺値を与えるとなぜか動く。C++2a Rangesライブラリは処理系任せの部分が大きく、トラブル時の振る舞いを説明しづらい。単に理解不足かorz

  • 2021-06-23追記:GCC HEAD 12.0.0 20210621(experimental)で再確認したところ、単にstd::views::split(delim) | ...と記述しても期待通りに動作した。

C++2a DIS(N4861) 24.7/p1, 24.7.1/p1より引用。

This subclause defines range adaptors, which are utilities that transform a range into a view with custom behaviors. These adaptors can be chained to create pipelines of range transformations that evaluate lazily as the resulting view is iterated.

A range adaptor closure object is a unary function object that accepts a viewable_range argument and returns a view. For a range adaptor closure object C and an expression R such that decltype((R)) models viewable_range, the following expressions are equivalent and yield a view:

C(R)
R | C

Given an additional range adaptor closure object D, the expression C | D is well-formed and produces another range adaptor closure object such that the following two expressions are equivalent:

R | C | D
R | (C | D)

関連URL

*1:厳密には std::ranges::views::split として宣言される。標準ヘッダ<ranges>にてstd名前空間以下で namespace views = ranges::views; と名前空間エイリアスが定義されている。

プライマリ変数テンプレート無効化の実装例

C++2a(C++20)標準ライブラリ<numbers>では、変数テンプレート(variable template)*1により浮動小数点型(float, double, long double)にあわせた数学定数を提供する。一方、浮動小数点型以外によるプライマリテンプレート利用はill-formedとなることが要請されている。

#include <numbers>

// float型精度の円周率π
float pi_f = std::numbers::pi_v<float>;  // OK
// int型の円周率π?
int pi_i = std::numbers::pi_v<int>;  // NG: ill-formed

N4861 (C++2a DIS) 26.9.2/p3より引用。

A program that instantiates a primary template of a mathematical constant variable template is ill-formed.

GCC/libstdc++

https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-v3/include/std/numbers

template<typename _Tp>
  using _Enable_if_floating = enable_if_t<is_floating_point_v<_Tp>, _Tp>;

template<typename _Tp>
  inline constexpr _Tp pi_v
    = _Enable_if_floating<_Tp>(3.14159...);

Clang/libcxx

https://github.com/llvm/llvm-project/blob/master/libcxx/include/numbers

template <class T>
inline constexpr bool __false = false;

template <class T>
struct __illformed
{
  static_assert(__false<T>, "...");
};

template <class T> inline constexpr T pi_v =  __illformed<T>{};

template <class T>
concept __floating_point = std::is_floating_point_v<T>;

template <__floating_point T> inline constexpr T pi_v<T> = 3.14159...;

MSVC

https://github.com/microsoft/STL/blob/master/stl/inc/numbers

// stl/inc/xstddef
template <class>
inline constexpr bool _Always_false = false;

template <class _Ty>
struct _Invalid {
  static_assert(_Always_false<_Ty>, "...");
};

template <class _Ty>
inline constexpr _Ty pi_v = _Invalid<_Ty>{};

template <floating_point _Floating>
inline constexpr _Floating pi_v<_Floating> = static_cast<_Floating>(3.14159...);

関連URL

*1:変数テンプレートは C++14 で追加された機能。

C互換ヘッダとstd名前空間

C++標準ライブラリに含まれるC標準ライブラリヘッダ*1と、std名前空間との関係についてメモ。

C++11以降では、下記の振る舞いが保証される:

  • ヘッダcxxx: 名前空間stdにCライブラリの識別子が宣言される。グローバル名前空間にも宣言されるかもしれない。
  • ヘッダxxx.h: グローバル名前空間にCライブラリの識別子が宣言される。名前空間stdにも宣言されるかもしれない。

C++17

C++17 D.5/p3-4より引用(下線部は強調)。29.9.5=Mathematical special functions*2, 21.2.5=byte type operations*3

3 Every other C header, each of which has a name of the form name.h, behaves as if each name placed in the standard library namespace by the corresponding cname header is placed within the global namespace scope, except for the functions described in 29.9.5, the declaration of std::byte (21.2.1), and the functions and function templates described in 21.2.5. It is unspecified whether these names are first declared or defined within namespace scope (6.3.6) of the namespace std and are then injected into the global namespace scope by explicit using-declarations (10.3.3).
4 [Example: The header <cstdlib> assuredly provides its declarations and definitions within the namespace std. It may also provide these names within the global namespace. The header <stdlib.h> assuredly provides the same declarations and definitions within the global namespace, much as in the C Standard. It may also provide these names within the namespace std. -- end example]

C++11/14

C++11 D.5/p2-3より引用(下線部は強調)。C++14でも同様。

2 Every C header, each of which has a name of the form name.h, behaves as if each name placed in the standard library namespace by the corresponding cname header is placed within the global namespace scope. It is unspecified whether these names are first declared or defined within namespace scope (3.3.6) of the namespace std and are then injected into the global namespace scope by explicit using-declarations (7.3.3).
3 [Example: The header <cstdlib> assuredly provides its declarations and definitions within the namespace std. It may also provide these names within the global namespace. The header <stdlib.h> assuredly provides the same declarations and definitions within the global namespace, much as in the C Standard. It may also provide these names within the namespace std. -- end example]

C++03

ヘッダcxxxでは名前空間std配下に宣言し、ヘッダxxx.hでは “名前空間std配下に宣言してからグローバル名前空間へ取り込む(using-declaration)” と強く規定されていた。C++03 D.5/p2-3より引用。

2 Every C header, each of which has a name of the form name.h, behaves as if each name placed in the Standard library namespace by the corresponding cname header is also placed within the namespace scope of the namespace std and is followed by an explicit using-declaration (7.3.3).
3 [Example: The header <cstdlib> provides its declarations and definitions within the namespace std. The header <stdlib.h> makes these available also in the global namespace, much as in the C Standard. -- end example]

関連URL

*1:ヘッダ <cstdlib> とヘッダ <stdlib.h> など

*2:assoc_laguerre, assoc_legendre, beta, comp_ellint_1, comp_ellint_2, comp_ellint_3, cyl_bessel_i, cyl_bessel_j, cyl_bessel_k, cyl_neumann, ellint_1, ellint_2, ellint_3, expint, hermite, laguerre, legendre, riemann_zeta, sph_bessel, sph_legendre, sph_neumann

*3:std::byte 型に対する演算子オーバーロード、to_integer<IntType>関数テンプレート

Concept-basedオーバーロードとSFINAE-unfriendlyメタ関数の落とし穴

C++2a(C++20)で導入されるrequires節を用いた関連制約(associated constraints)により従来SFINAE技法よりも関数テンプレートのオーバーロード制御が容易となるが、戻り値型にSFINAE-unfriendlyなメタ関数を用いるケースでは意図しないコンパイルエラーを引き起こす。

2021-01-13追記:CWG 2369. Ordering between constraints and substitution にてConceptとSFINAEの差異をなくす修正提案がなされ、N4879 によれば DefectReport として採択済み。C++20から遡及適用されるため、本記事の内容は全て無効となる。☆(ゝω・)vヤッタネ

本記事の内容はStackOverflowで見つけた質問と回答に基づく。

下記コードにおいてdouble型の値1.0test関数を呼び出すと、オーバーロード#1の戻り値型導出にて make_signedメタ関数 の必須要件(Mandates)違反によるコンパイルエラーとなる(→id:yohhoy:20200605)。*1

// C++2a
#include <concepts>
#include <type_traits>

// template <typename T>
//   requires std::unsigned_integral<T>と等価
template <std::unsigned_integral T>
auto test(T) -> std::make_signed_t<T>;  // #1

template <typename T>
auto test(T) -> int;  // #2

test(1u);   // OK: #1を選択
test(1.0);  // NG!? (#2を期待)

この振る舞いは 1) 実引数からのテンプレートパラメータ推論とオーバーロード候補除外(SFINAE-based overloading)、2) テンプレートパラメータに依存する戻り値型導出、3) 関連制約に基づくオーバーロード候補除外(Concept-based overloading)という順序に起因している。

前掲コードを古き良きSFINAE技法に書き換えると、オーバーロード#1の戻り値型導出よりも前に候補除外されstd::make_signedメタ関数の必須要件違反は回避される。一方でrequires節に比べると煩雑なコード記述が必要とされる。

#include <concepts>
#include <type_traits>

template <typename T,
          typename = std::enable_if_t<std::unsigned_integral<T>>>
auto test(T) -> std::make_signed_t<T>;  // #1

template <typename T,
          typename = std::enable_if_t<!unsigned_integral<T>>>
auto test(T) -> int;  // #2

test(1u);   // OK: #1を選択
test(1.0);  // OK: #2を選択

StackOverflow回答によれば本件は CWG 2369 にて議論中とのこと。2020年9月現在は最新ステータス非公開...

C++2a DIS(N4861) 13.10.2/p5より引用(下線部は強調)。

The resulting substituted and adjusted function type is used as the type of the function template for template argument deduction. If a template argument has not been deduced and its corresponding template parameter has a default argument, the template argument is determined by substituting the template arguments determined for preceding template parameters into the default argument. If the substitution results in an invalid type, as described above, type deduction fails. [Example:

 template <class T, class U = double>
 void f(T t = 0, U u = 0);

 void g() {
   f(1, 'c');      // f<int,char>(1,'c')
   f(1);           // f<int,double>(1,0)
   f();            // error: T cannot be deduced 
   f<int>();       // f<int,double>(0,0)
   f<int,char>();  // f<int,char>(0,0)
}

-- end example]
When all template arguments have been deduced or obtained from default template arguments, all uses of template parameters in the template parameter list of the template and the function type are replaced with the corresponding deduced or default argument values. If the substitution results in an invalid type, as described above, type deduction fails. If the function template has associated constraints (13.5.2), those constraints are checked for satisfaction (13.5.1). If the constraints are not satisfied, type deduction fails.

関連URL

*1:実際の振る舞いはC++標準ライブラリ内部実装に依存する。処理系によってはオーバーロード#1が ill-formed とならずに、期待通りオーバーロード#2が選択される可能性もある。

memcmp関数 × 宇宙船演算子

C++2a(C++20)で導入される三方比較演算子(three-way comparison operator)<=>(通称:宇宙船演算子(spaceship operator))と、C言語時代の関数インタフェースでよくみられる三方比較結果 “負値/値0/正値” との組合せ利用について。新旧仕様の橋渡し。

例えばmemcmp関数によるメモリブロック比較結果(int型)は、さらに値0と三方比較することでstrong_ordering型の比較結果へと変換できる。

// C++2a
#include <cstring>  // memcmp
#include <compare>

auto memblock_compare(const void* p, const void* q, size_t n)
  -> std::strong_ordering
{
  return std::memcmp(p, q, n) <=> 0;
  // 下記処理に相当
  // int r = std::memcmp(p, q, n);
  // if (r < 0) return std::strong_ordering::less;
  // if (r > 0) return std::strong_ordering::greater;
  // return std::strong_ordering::equal;
}

ノート:C++標準ライブラリstd::basic_string, std::basic_string_view, std::filesystem::pathcompareメンバ関数も三方比較結果を戻り値 “負値/値0/正値” として返すが、C++2aでは<=>演算子オーバーロードも直接提供するため本テクニックの利用機会はないだろう。

関連URL

todo!とunimplemented!の違い

プログラミング言語Rustのtodo!マクロとunimplemented!マクロの違い。

  • todo!:作業途中のコード。あとで実装する。
  • unimplemented!:現行コードでは未実装(実装するとは言っていない)

The difference between unimplemented! and todo! is that while todo! conveys an intent of implementing the functionality later and the message is "not yet implemented", unimplemented! makes no such claims. Its message is "not implemented". Also some IDEs will mark todo!s.

関連URL

複合要件とsame_as/convertible_toコンセプト

C++2a(C++20)コンセプト requires式(requires-expression) に記述する 複合要件(compound-requirement) では「ある式の評価結果が特定コンセプトを満たすこと」を制約するが、このとき標準コンセプトstd::same_asstd::convertible_toを適切に使い分ける必要がある。特に “データメンバ型の制約” には注意すること。

下記コードのコンセプトC0では式t.dataint型へと変換可能(convertible_to<int>)と制約しているが、コンセプトC1のようにint型と等しい(same_as<int>)と制約するとプログラマの期待通りに動作しない。

// C++2a
#include <concepts>

struct S {
  int data;   // int型のデータメンバ
  int& mf();  // int&型を返すメンバ関数
};

template <typename T> concept C0 = requires (T t) {
  // T型の値tはデータメンバdataをもち、int型へと変換可能
  { t.data } -> std::convertible_to<int>;
  // T型の値tはメンバ関数mf()をもち、呼び出し結果はint&型に等しい
  { t.mf() } -> std::same_as<int&>;
};
static_assert( C0<S> );  // OK

template <typename T> concept C1 = requires (T t) {
  // T型の値tはデータメンバdataをもち、"式t.data"はint型に等しい
  { t.data } -> std::same_as<int>;
  // ...
};
static_assert( C1<S> );  // NG: 式t.dataの型はint&

戻り値型を制約する複合要件{ E } -> C<U>;は、2つの制約E; requires C<decltype((E)), U>;*1と等価である。ここでdecltype指定子オペランドが括弧付きの式(E)となることに留意。(→id:yohhoy:20200817

template <typename T> concept C1 = requires (T t) {
  // { t.data } -> std::same_as<int>; と等価な制約
  t.data;
  requires std::same_as<decltype((t.data)), int>;
  // ...
};

クラスのデータメンバ型をsame_asコンセプトで制約する場合は、下記コンセプトC2のように入れ子要件(nested-requirement)を利用する。*2

template <typename T> concept C2 = requires {
  // メンバT::dataはint型に等しい
  requires std::same_as<decltype(T::data), int>;
  // ...
}
static_assert( C2<S> );  // OK

下記コンセプトC3のように複合要件とsame_asコンセプトを組み合わせた場合、データメンバ型はintまたはint&いずれも制約を満たす。これはsame_asコンセプト利用意図からすると、不適切な制約表現といえる。*3

struct S1 {
  int data;   // int型のデータメンバ
};
struct S2 {
  int& data;  // int&型のデータメンバ
};

template <typename T> concept C3 = requires (T t) {
  { t.data } -> std::same_as<int&>;
};

// OK: データメンバが int型 または int&型 であれば制約を満たす
static_assert( C3<S1> && C3<S2> ); 

N4861 7.5.7.3/p1, 18.4.2, 18.4.4/p1より一部引用。

compound-requirement:
  { expression } noexceptopt return-type-requirementopt ;
return-type-requirement:
  -> type-constraint


A compound-requirement asserts properties of the expression E. Substitution of template arguments (if any) and verification of semantic properties proceed in the following order:

  • Substitution of template arguments (if any) into the expression is performed.
  • If the noexcept specifier is present, E shall not be a potentially-throwing expression (14.5).
  • If the return-type-requirement is present, then:
    • Substitution of template arguments (if any) into the return-type-requirement is performed.
    • The immediately-declared constraint (13.2) of the type-constraint for decltype((E)) shall be satisfied. [Example: Given concepts C and D,
requires {
  { E1 } -> C;
  { E2 } -> D<A1, ..., An>;
};

is equivalent to

requires {
  E1; requires C<decltype((E1))>;
  E2; requires D<decltype((E2)), A1, ..., An>;
};

(including in the case where n is zero). -- end example]

template<class T, class U>
  concept same-as-impl = is_same_v<T, U>;  // exposition only
template<class T, class U>
  concept same_as = same-as-impl <T, U> && same-as-impl <U, T>;

[Note: same_as<T, U> subsumes same_as<U, T> and vice versa. -- end note]

Given types From and To and an expression E such that decltype((E)) is add_rvalue_reference_t<From>, convertible_to<From, To> requires E to be both implicitly and explicitly convertible to type To. The implicit and explicit conversions are required to produce equal results.

template<class From, class To>
  concept convertible_to =
    is_convertible_v<From, To> &&
    requires(add_rvalue_reference_t<From> (&f)()) {
      static_cast<To>(f());
    };

関連URL

*1:単純要件(simple-requirement)と入れ子要件(nested-requirement)の2つで表現される。

*2:制約が std::same_as<decltype(T::data), int> のみで構成される場合は requires 式の入れ子要件とする必要はなく、直接 template <typename T> concept C2 = std::same_as<decltype(T::data), int>; と記述すればよい。

*3:ソースコード上は “参照型と等しい” と読み取れる記述がなされ、実際には参照型/値型の両方を許容するコンセプトはトラブルの元になる可能性が高い。