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

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

struct X {};

auto foo(X... args)

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

Самое близкое у нас это:

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

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

39

Решение

Да, это возможно. Прежде всего вам необходимо решить, хотите ли вы принимать только тип, или вы хотите принять неявно конвертируемый тип. я использую std::is_convertible в примерах, потому что это лучше имитирует поведение не шаблонных параметров, например, 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

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

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

Другие решения

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

Вы также можете использовать check в сочетании с std::enable_if_t в качестве типа возврата, если вы не хотите использовать static_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 также в типе возврата или где вы хотите.

5

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

— РЕДАКТИРОВАТЬ — Улучшено следующее предложение от болов и 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 в этом примере).

4

У вас уже есть, начиная с C ++ 11 стандарта.

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

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

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

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

1

Как насчет 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;
}

Еще один c ++ 11, в котором используется решение «one-liner» на основе 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