Подтвердить что ты не робот

Ограничить переменные аргументы шаблона

Можем ли мы ограничить переменные аргументы шаблона определенным типом? Я., добиваюсь чего-то подобного (не настоящий С++, конечно):

struct X {};

auto foo(X... args)

Здесь мое намерение состоит в том, чтобы иметь функцию, которая принимает переменное число параметров X.

Самое близкое, что у нас есть:

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

но это принимает любой тип параметра.

4b9b3361

Ответ 1

Да, это возможно. Прежде всего, вам нужно решить, хотите ли вы принять только тип, или если вы хотите принять неявно конвертируемый тип. Я использую std::is_convertible в примерах, потому что он лучше имитирует поведение не templated параметров, например. Параметр long long принимает аргумент int. Если по какой-либо причине вам нужен только тот тип, который нужно принять, замените std::is_convertible на std:is_same (вам может потребоваться добавить std::remove_reference и std::remove_cv).

К сожалению, в C++ сужение преобразования, например. (long long до int и даже duble до 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
};

Концепция

Не здесь, но скоро. Это будет самое простое, ясное и элегантное решение

template <class From, class To>
concept constexpr bool Convertible = std::is_convertible_v<From, To>;

template <Convertible<X>... Args>
auto foo_x(Args... args) {}

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

Мы получаем очень приятную ошибку. Особенно

'Кабриолет' не был удовлетворен

сладкий:

error: cannot call function 'auto foo_x(Args ...) [with Args = {X, X, Y, Derived, Z}]'
     foo_x(x, x, y, d, z);
                        ^
note:   constraints not satisfied
auto foo_x(Args... args)
     ^~~~~
note: in the expansion of 'Convertible<Args, X>...'
note:     'Convertible<Z, X>' was not satisfied

Работа с сужением:

template <class From, class To>
concept constexpr bool Convertible_no_narrow = requires(From f, To t) {
    t = {f};
};

template <Convertible_no_narrow<int>... Args>
auto foo_ni(Args... args) {}

foo_ni(24, 12); // OK
foo_ni(24, 12, 15.2);
// error:
// 'Convertible_no_narrow<double, int>' was not satisfied

С++ 17

Мы используем очень приятное fold выражение:

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> {};

С++ 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

С++ 11

незначительные изменения в версии С++ 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

Ответ 2

Как насчет следующего решения?

--- 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), но (рекурсивно) реализовать его только для одного типа (int в этом примере).

Ответ 3

С++ 14

С С++ 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>();
}

Вы также можете использовать check в сочетании с std::enable_if_t как возвращаемый тип, если вы не хотите использовать static_assert по неизвестным причинам:

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

И так далее...

С++ 11

В С++ 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 также в возвращаемом типе или где хотите.

Ответ 4

Как насчет static_assert и метода вспомогательного шаблона (решение С++ 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;
}

Еще один С++ 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) {
}

Ответ 5

У вас уже есть это с С++ 11.

Простой std::array (частный случай std::tuple, где все элементы кортежа имеют один и тот же тип) будет достаточным.

Однако, если вы хотите использовать его в функции шаблона, вы можете лучше использовать'std:: initializer_list`, как в следующем примере:

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

Это действительно простое решение, которое решает вашу проблему. Использование аргументов Variadic шаблона также является опцией, но добавляет излишнюю сложность для вашего кода. Помните, что ваш код должен быть доступен для чтения другими, включая себя через некоторое время.