はじめに
C++20からコンセプト(concepts)が導入されましたが、それ以前のバージョンのC++でもコンセプトを使いたいというのは全人類共通の願いだと思います。この記事は、C++14でもできるだけコンセプトを再現しようという試みです。
実装の方針
基本的な構造は以下のようになります。
# if defined(__cpp_concepts) && (__cpp_concepts >= 201907)
# include <concepts>
namespace cpp14_concepts
{
using std::same_as;
}
# else
namespace cpp14_concepts
{
// C++14での実装
}
# endif
C++20 Concepts が使える場合はstdのものをusingして、使えない場合は自前での実装となります。C++20側の記述は基本的に一緒なので、これ以降の記事では必要がない限り省略します。また、cpp14_concepts ネームスペースも省略します。
まずは簡単なものを実装
template <typename T, typename U>
constexpr bool same_as = std::is_same<T, U>::value;
template <typename Derived, typename Base>
constexpr bool derived_from =
std::is_base_of<Base, Derived>::value &&
std::is_convertible<const volatile Derived*, const volatile Base*>::value;
template <typename T>
constexpr bool integral = std::is_integral<T>::value;
template <typename T>
constexpr bool signed_integral = integral<T> && std::is_signed<T>::value;
template <typename T>
constexpr bool unsigned_integral = integral<T> && !signed_integral<T>;
template <typename T>
constexpr bool floating_point = std::is_floating_point<T>::value;
template <typename T>
constexpr bool destructible = std::is_nothrow_destructible<T>::value;
template <typename T, typename... Args>
constexpr bool constructible_from =
destructible<T> && std::is_constructible<T, Args...>::value;
<concepts>ヘッダのうち以上のコンセプトは、変数テンプレートを使えば簡単に実装できます。1基本的にはC++20 Concepts での定義をそのまま変数テンプレートに変えただけです。2
bool型の定数として使う
bool型の定数として使う場合は、以下のようにC++14/17/20どれでも同じ記述で使うことができます。
static_assert(same_as<int, int> == true, "");
static_assert(same_as<int, long> == false, "");
class Base {};
class Derived: public Base {};
static_assert(derived_from<Base, Derived> == false, "");
static_assert(derived_from<Derived, Base> == true, "");
static_assert(integral<int> == true, "");
static_assert(integral<float> == false, "");
static_assert(signed_integral<signed int> == true, "");
static_assert(signed_integral<unsigned int> == false, "");
static_assert(unsigned_integral<signed int> == false, "");
static_assert(unsigned_integral<unsigned int> == true, "");
static_assert(floating_point<int> == false, "");
static_assert(floating_point<float> == true, "");
struct A {};
struct B { ~B() noexcept(true) {} };
struct C { ~C() noexcept(false) {} };
static_assert(destructible<A> == true, "");
static_assert(destructible<B> == true, "");
static_assert(destructible<C> == false, "");
struct S
{
S(int){}
S(int, double){}
};
static_assert(constructible_from<S, int> == true, "");
static_assert(constructible_from<S, int, double> == true, "");
static_assert(constructible_from<S, int, double, int> == false, "");
テンプレートパラメータを制約する
C++20 Concepts でテンプレートパラメータを制約するとき、以下の選択肢があります。(cpprefjpより)
-
class
/typename
の代わりにコンセプトを指定する -
requires
節を使用する - 関数テンプレートの簡略構文を使用する
2と3をC++14で再現する方法は私の頭では思いつかなかったので、1を再現していこうと思います。といっても、C++20以前でテンプレートパラメータを制約するといえば、おなじみの enable_if を使っていくだけです。
template <
# if defined(__cpp_concepts) && (__cpp_concepts >= 201907)
integral T
# else
typename T,
typename = std::enable_if_t<integral<T>>
# endif
>
void f(T);
このようにすればC++14でもC++20でも同じようにテンプレートパラメータを制約できるわけですが、毎回 #if ~ #else ~ #endif を書くのはだるいので、マクロを作ります。3
# if defined(__cpp_concepts) && (__cpp_concepts >= 201907)
# define REQUIRED_PARAM(C, T) C T
# else
# define REQUIRED_PARAM(C, T) typename T, std::enable_if_t<C<T>>* = nullptr
# endif
これを使うと、前掲の関数は以下のように書けます。
template <REQUIRED_PARAM(integral, T)>
void f(T);
だいぶスッキリしました。
引数が2個以上あるコンセプトにも対応する
さきほどのマクロでは、引数が1個のコンセプトにしか対応していませんでしたので、2個以上にも対応できるようにマクロを改良します。
# define PP_JOIN(X, Y) PP_DO_JOIN(X, Y)
# define PP_DO_JOIN(X, Y) PP_DO_JOIN2(X,Y)
# define PP_DO_JOIN2(X, Y) X##Y
# define PP_EXPAND(x) x
# define PP_VA_ARG_COUNT(...) PP_EXPAND(PP_VA_ARG_DO_COUNT(__VA_ARGS__, 9,8,7,6,5,4,3,2,1,0))
# define PP_VA_ARG_DO_COUNT(_1,_2,_3,_4,_5,_6,_7,_8,_9,N,...) N
# define REQUIRED_PARAM(C, ...) PP_EXPAND(PP_JOIN(REQUIRED_PARAM_, PP_VA_ARG_COUNT(__VA_ARGS__))(C, __VA_ARGS__))
# if defined(__cpp_concepts) && (__cpp_concepts >= 201907)
# define REQUIRED_PARAM_1(C, T) C T
# define REQUIRED_PARAM_2(C, T1, T2) C<T1> T2
# define REQUIRED_PARAM_3(C, T1, T2, T3) C<T1, T2> T3
# else
# define REQUIRED_PARAM_1(C, T) typename T, std::enable_if_t<C<T>>* = nullptr
# define REQUIRED_PARAM_2(C, T1, T2) typename T2, std::enable_if_t<C<T2, T1>>* = nullptr
# define REQUIRED_PARAM_3(C, T1, T2, T3) typename T3, std::enable_if_t<C<T3, T1, T2>>* = nullptr
# endif
PP_JOIN は BOOST_JOINでおなじみの2つのシンボルを合体するマクロ、PP_VA_ARG_COUNT は __VA_ARGS__に与えられた引数の個数を返すマクロです。4
REQUIRED_PARAM では引数の個数をかぞえ、REQUIRED_PARAM_1, REQUIRED_PARAM_2, REQUIRED_PARAM_3 に処理を振り分けます。
今回は引数3つまでしか対応していませんが、それ以上に拡張するのは簡単ですね。
これを使うと引数が2個以上あるコンセプトでも同じように書くことができます。
template <REQUIRED_PARAM(integral, T)>
void f1(T);
template <REQUIRED_PARAM(constructible_from, int, T)>
void f2(T);
template <REQUIRED_PARAM(constructible_from, int, int, T)>
void f3(T);
オーバーロードの優先順位
ここまで書いたコードを使って、関数テンプレートをコンセプトによって実装を振り分けたいので、以下のように書いたとします。
// (A)
template <REQUIRED_PARAM(integral, T)>
int f1(T) { return 0; }
// (B)
template <REQUIRED_PARAM(unsigned_integral, T)>
int f1(T) { return 1; }
int main()
{
std::cout << f1(42) << std::endl; // 0
std::cout << f1(42u) << std::endl; // 1
return 0;
}
C++20では期待どおり動作しますが、C++14では関数呼び出しがあいまい(ambiguous)だとしてコンパイルエラーになります。
enable_ifで制約した場合、unsigned int 型はintegralコンセプトとunsigned_integralコンセプトの両方を満たすため、(A)と(B)のオーバーロード優先順位は同じとなり、関数呼び出しがあいまいになってしまいます。
一方、C++20 Concepts では、より制約の厳しいコンセプトほどオーバーロード優先順位が高くなるため、(B)の呼び出しのほうが優先順位が高くなり、問題なくコンパイルできます。
これを解決するために、ちょっとしたクラステンプレートを追加します。
template <std::size_t N>
struct overload_priority : public overload_priority<N - 1>
{};
template <>
struct overload_priority<0>
{};
そして先程のf1関数を次のように書き換えます。
template <REQUIRED_PARAM(integral, T)>
int f1_impl(T, overload_priority<0>) { return 0; }
template <REQUIRED_PARAM(unsigned_integral, T)>
int f1_impl(T, overload_priority<1>) { return 1; }
template <typename T>
int f1(T t)
{
return f1_impl(t, overload_priority<1>{});
}
こうすることによって、enable_ifの場合でも優先順位の問題が解決され、呼び出し側はC++14/17/20すべてで共通化することができます。
まとめ
標準ライブラリにあるコンセプトのうち、まずは実装するのが簡単なものを作っていきました。それだけで結構な文章量になってしまったので、次回に続きます。次回以降は、標準ライブラリにあるコンセプトのうち、実装するのが少し難しいものについて実装していこうと思います。
Githubにcpp14_conceptsというリポジトリを作りました。この記事までの内容は v0.1 タグで見ることができます。