web-dev-qa-db-ja.com

可変テンプレートの引数を制限する

可変テンプレートの引数を特定の型に制限できますか?つまり、次のようなものを実現します(もちろん実際のC++ではありません)。

struct X {};

auto foo(X... args)

ここでの私の意図は、可変数のXパラメーターを受け入れる関数を持つことです。

私たちが最も近いのはこれです:

template <class... Args>
auto foo(Args... args)

ただし、これは任意のタイプのパラメーターを受け入れます。

41
bolov

はい、可能です。最初に、型のみを受け入れるか、暗黙的に変換可能な型を受け入れるかを決定する必要があります。例ではstd::is_convertibleを使用しています。これは、テンプレート化されていないパラメータの動作をよりよく模倣するためです。 long longパラメータはint引数を受け入れます。何らかの理由でそのタイプのみを受け入れる必要がある場合は、std::is_convertiblestd:is_sameに置き換えます(std::remove_referencestd::remove_cvの追加が必要になる場合があります)。

残念ながら、C++では、変換を狭めています。 (long longからint、さらにdoubleからint)は暗黙的な変換です。また、従来の設定では警告が発生したときに警告が表示されますが、std::is_convertibleでは警告が表示されません。少なくとも呼び出しではありません。このような割り当てを行うと、関数の本文に警告が表示される場合があります。しかし、ちょっとしたトリックで、呼び出しサイトでテンプレートを使用してもエラーが発生する可能性があります。

したがって、ここでさらに苦労することなく、次のようになります。


テスト装置:

struct X {};
struct Derived : X {};
struct Y { operator X() { return {}; }};
struct Z {};

foo_x : function that accepts X arguments

int main ()
{
   int i{};
   X x{};
   Derived d{};
   Y y{};
   Z z{};

   foo_x(x, x, y, d); // should work
   foo_y(x, x, y, d, z); // should not work due to unrelated z
};

C++ 20の概念

まだここではありませんが、すぐに。 gccトランクで利用可能(2020年3月)。これは、最もシンプルで、明確で、エレガントで安全なソリューションです。

#include <concepts>

auto foo(std::convertible_to<X> auto ... args) {}

foo(x, x, y, d); // OK
foo(x, x, y, d, z); // error:

非常にいいエラーが出ます。特に

制約が満たされていない

甘いです:

ナローイングへの対処:

ライブラリにコンセプトが見つからなかったため、作成する必要があります。

template <class From, class To>
concept ConvertibleNoNarrowing = std::convertible_to<From, To>
    && requires(void (*foo)(To), From f) {
        foo({f});
};

auto foo_ni(ConvertibleNoNarrowing<int> auto ... args) {}

foo_ni(24, 12); // OK
foo_ni(24, (short)12); // OK
foo_ni(24, (long)12); // error
foo_ni(24, 12, 15.2); // error

C++ 17

私たちは非常に素晴らしい fold expression を利用しています:

template <class... Args,
         class Enable = std::enable_if_t<(... && std::is_convertible_v<Args, X>)>>
auto foo_x(Args... args) {}

foo_x(x, x, y, d, z);    // OK
foo_x(x, x, y, d, z, d); // error

残念ながら、あまり明確ではないエラーが発生します。

テンプレート引数の控除/置換に失敗しました:[...]

狭める

ナローイングは回避できますが、トレイトis_convertible_no_narrowingをクックする必要があります(おそらく別の名前を付けます)。

template <class From, class To>
struct is_convertible_no_narrowing_impl {
  template <class F, class T,
            class Enable = decltype(std::declval<T &>() = {std::declval<F>()})>
  static auto test(F f, T t) -> std::true_type;
  static auto test(...) -> std::false_type;

  static constexpr bool value =
      decltype(test(std::declval<From>(), std::declval<To>()))::value;
};

template <class From, class To>
struct is_convertible_no_narrowing
    : std::integral_constant<
          bool, is_convertible_no_narrowing_impl<From, To>::value> {};

C++ 14

結合ヘルパーを作成します。
C++17にはstd::conjunctionがありますが、std::integral_constant引数が必要です。

template <bool... B>
struct conjunction {};

template <bool Head, bool... Tail>
struct conjunction<Head, Tail...>
    : std::integral_constant<bool, Head && conjunction<Tail...>::value>{};

template <bool B>
struct conjunction<B> : std::integral_constant<bool, B> {};

そして今、私たちは私たちの機能を持つことができます:

template <class... Args,
          class Enable = std::enable_if_t<
              conjunction<std::is_convertible<Args, X>::value...>::value>>
auto foo_x(Args... args) {}


foo_x(x, x, y, d); // OK
foo_x(x, x, y, d, z); // Error

C++ 11

c ++ 14バージョンへのマイナーな調整:

template <bool... B>
struct conjunction {};

template <bool Head, bool... Tail>
struct conjunction<Head, Tail...>
    : std::integral_constant<bool, Head && conjunction<Tail...>::value>{};

template <bool B>
struct conjunction<B> : std::integral_constant<bool, B> {};

template <class... Args,
          class Enable = typename std::enable_if<
              conjunction<std::is_convertible<Args, X>::value...>::value>::type>
auto foo_x(Args... args) -> void {}

foo_x(x, x, y, d); // OK
foo_x(x, x, y, d, z); // Error
47
bolov

C++ 14

C++ 14以降では、変数テンプレート、部分的な特殊化、およびstatic_assertも使用できます。例として:

#include <type_traits>

template<template<typename...> class, typename...>
constexpr bool check = true;

template<template<typename...> class C, typename U, typename T, typename... O>
constexpr bool check<C, U, T, O...> = C<T, U>::value && check<C, U, O...>;

template<typename... T>
void f() {
    // use std::is_convertible or whichever is the best trait for your check
    static_assert(check<std::is_convertible, int, T...>, "!");
    // ...
}

struct S {};

int main() {
    f<int, unsigned int, int>();
    // this won't work, for S is not convertible to int
    // f<int, S, int>();
}

不明な理由でstd::enable_if_tを使用したくない場合は、checkstatic_assertと組み合わせて戻り値の型として使用することもできます。

template<typename... T>
std::enable_if_t<check<std::is_convertible, int, T...>>
f() {
    // ...
}

等々...

C++ 11

C++ 11では、受け入れられない型に遭遇したときに再帰を直ちに停止するソリューションを設計することもできます。例として:

#include <type_traits>

template<bool...> struct check;
template<bool... b> struct check<false, b...>: std::false_type {};
template<bool... b> struct check<true, b...>: check<b...> {};
template<> struct check<>: std::true_type {};

template<typename... T>
void f() {
    // use std::is_convertible or whichever is the best trait for your check
    static_assert(check<std::is_convertible<int, T>::value...>::value, "!");
    // ...
}

struct S {};

int main() {
    f<int, unsigned int, int>();
    // this won't work, for S is not convertible to int
    // f<int, S, int>();
}

上記のように、戻り値の型やどこでもcheckを使用できます。

6
skypjack

次の解決策はどうですか?

--- EDIT --- bolovとJarod42からの提案に従って改善(ありがとう!)

_#include <iostream>

template <typename ... Args>
auto foo(Args... args) = delete;

auto foo ()
 { return 0; }

template <typename ... Args>
auto foo (int i, Args ... args)
 { return i + foo(args...); }

int main () 
 {
   std::cout << foo(1, 2, 3, 4) << std::endl;  // compile because all args are int
   //std::cout << foo(1, 2L, 3, 4) << std::endl; // error because 2L is long

   return 0;
 }
_

foo()を宣言して、すべてのタイプの引数(_Args ... args_)を受け取ることができますが、(再帰的に)1つのタイプ(この例ではint)に対してのみ実装します。

4
max66

static_assertとヘルパーテンプレートメソッド(c ++ 11ソリューション):

template <bool b>
int assert_impl() {
   static_assert(b, "not convertable");
   return 0;
}

template <class... Args>
void foo_x(Args... args) {
    int arr[] {assert_impl<std::is_convertible<Args, X>::value>()...};
    (void)arr;
}

もう1つc ++ 11は、「ワンライナー」sfinaeベースのソリューションを使用しています。

template <class... Args,
          class Enable = decltype(std::array<int, sizeof...(Args)>{typename std::enable_if<std::is_convertible<Args, X>::value, int>::type{}...})>
void foo_x(Args... args) {
}
1
W.F.

C++ 11標準以降、すでに使用しています。

単純なstd::array(すべてのタプル要素が同じ型を共有するstd::Tupleの特殊なケース)で十分です。

ただし、テンプレート関数で使用する場合は、次の例のように´std :: initializer_list`を使用することをお勧めします。

template< typename T >
void foo( std::initializer_list<T> elements );

これはあなたの問題を解決する本当にシンプルなソリューションです。可変テンプレートの引数を使用することもオプションですが、コードに不要な複雑さを追加します。あなたのコードは、しばらくすると自分自身を含め、他の人が読めるはずです。

1
Jorge Bellon