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

Пакет параметров С++, ограниченный экземплярами одного типа?

С С++ 11 мы можем создавать функции шаблона, которые могут принимать любую последовательность аргументов:

template <typename... Ts>
void func(Ts &&... ts) {
   step_one(std::forward<Ts>(ts)...);
   step_two(std::forward<Ts>(ts)...);
}

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

Какой лучший способ сделать это, т.е. есть хороший способ ограничить шаблоны, чтобы сделать приятное сообщение об ошибке в этом случае или, в идеале, исключить func от участия в разрешении перегрузки, когда аргументы не совпадают


Я могу сделать это действительно конкретным, если это поможет:

Предположим, что у меня есть некоторая структура:

struct my_struct {
  int foo;
  double bar;
  std::string baz;
};

Теперь я хочу иметь возможность делать такие вещи, как печатать члены структуры для целей отладки, сериализовать и десериализовать структуру, посещать членов структуры последовательно и т.д. У меня есть код, который поможет с этим

template <typename V>
void apply_visitor(V && v, my_struct & s) {
  std::forward<V>(v)("foo", s.foo);
  std::forward<V>(v)("bar", s.bar);
  std::forward<V>(v)("baz", s.baz);
}

template <typename V>
void apply_visitor(V && v, const my_struct & s) {
  std::forward<V>(v)("foo", s.foo);
  std::forward<V>(v)("bar", s.bar);
  std::forward<V>(v)("baz", s.baz);
}

template <typename V>
void apply_visitor(V && v, my_struct && s) {
  std::forward<V>(v)("foo", std::move(s).foo);
  std::forward<V>(v)("bar", std::move(s).bar);
  std::forward<V>(v)("baz", std::move(s).baz);
}

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

Итак, теперь я хотел бы расширить его, чтобы он мог одновременно посещать два экземпляра my_struct. Использование этого в том, что если я хочу реализовать операции равенства или сравнения. В документации boost::variant они называют это "двоичное посещение" в отличие от "унарных посещений".

Вероятно, никто не захочет делать больше, чем бинарное посещение. Но предположим, что я хочу сделать, вообще, n-ary посещение. Тогда, похоже, я думаю,

template <typename V, typename ... Ss>
void apply_visitor(V && v, Ss && ... ss) {
  std::forward<V>(v)("foo", (std::forward<Ss>(ss).foo)...);
  std::forward<V>(v)("bar", (std::forward<Ss>(ss).bar)...);
  std::forward<V>(v)("baz", (std::forward<Ss>(ss).baz)...);
}

Но теперь это становится немного более безвозвратно - если кто-то передает серию типов, которые даже не имеют одинакового типа структуры, код все еще может компилироваться и делать что-то совершенно неожиданное для пользователя.

Я думал о том, как это сделать:

template <typename V, typename ... Ss>
void apply_visitor(V && v, Ss && ... ss) {
  auto foo_ptr = &my_struct::foo;
  std::forward<V>(v)("foo", (std::forward<Ss>(ss).*foo_ptr)...);
  auto bar_ptr = &my_struct::bar;
  std::forward<V>(v)("bar", (std::forward<Ss>(ss).*bar_ptr)...);
  auto baz_ptr = &my_struct::baz;
  std::forward<V>(v)("baz", (std::forward<Ss>(ss).*baz_ptr)...);
}

Это, по крайней мере, вызовет ошибку компиляции, если они будут использовать ее с несоответствующими типами. Но это также происходит слишком поздно - это происходит после того, как типы шаблонов решены, и после разрешения перегрузки я предполагаю.

Я думал об использовании SFINAE, например, вместо возврата void, используя std::enable_if_t и проверяя некоторое выражение std::is_same<std::remove_cv_t<std::remove_reference_t<...>> для каждого типа в пакете параметров.

Но для одного это выражение SFINAE довольно сложно, а для двоих оно также имеет недостаток - предположим, что у кого-то есть производный класс struct my_other_struct : my_struct { ... }, и они хотят использовать его с механизмом посетителя, поэтому некоторые из параметры my_struct, а некоторые - my_other_struct. В идеале система конвертирует все ссылки на my_struct и применяет посетителя таким образом, а афайк, который я привел выше, с указателями элементов foo_ptr, bar_ptr, baz_ptr сделал бы там правильные вещи, но это даже не ясно, как написать ограничение, подобное этому с SFINAE - мне нужно будет попытаться найти общую базу всех параметров, которые я предполагаю?

Есть ли хороший способ примирить эти проблемы в целом?

4b9b3361

Ответ 1

С std::common_type это просто:

template <class... Args, class = std::common_type_t<Args...>>
void foo(Args &&... args) {

}

Это будет гарантировано только SFINAE -дружественно от C++17. Clang и GCC уже реализованы так.

Ответ 2

Вот черта типа, которую вы можете использовать в static_assert или std::enable_if на досуге.

template <class T, class ... Ts>
struct are_all_same : conjunction<std::is_same<T, Ts>...>{};

template <class Ts...>
struct conjunction : std::true_type{};

template <class T, class ... Ts>
struct conjunction<T, Ts...> :
    std::conditional<T::value, conjunction<Ts...>, std::false_type>::type {};

Он просто проверяет каждый тип с первым и терпит неудачу, если он отличается.

Я думаю, что использование std::common_type будет выглядеть примерно так:

    template <class ... Args>
    typename std::common_type<Args...>::type common_type_check(Args...);

    void common_type_check(...);

    template <class ... Ts>
    struct has_common_type :
        std::integral_constant<
            bool,
            !std::is_same<decltype(common_type_check(std::declval<Ts>()...)), void>::value> {};

Затем вы можете сделать static_assert(std::has_common_type<Derived, Base>::value, "")

Конечно, этот метод не является надежным, поскольку common_type имеет некоторые ограничения, когда дело доходит до базовых классов:

struct A    {};
struct B : A{};
struct C : A{};
struct D : C{};
struct E : B{};

static_assert(has_common_type<E, D, C, A, B>::value, ""); //Fails
static_assert(has_common_type<A, B, C, D, E>::value, ""); //Passes

Это связано с тем, что шаблон сначала пытается получить общий тип между D и E (т.е. auto a = bool() ? D{}: E{}; не удается скомпилировать).

Ответ 3

То, что вы действительно хотите, это что-то вроде:

template<typename T, T ... args>
void myFunc(T ... args);

Но, очевидно, это не юридический синтаксис. Однако вы можете обойти эту проблему с помощью шаблона using, который поможет вам. Итак, идея такова:

template<typename T, size_t val>
using IdxType = T;

Вышеизложенное не имеет реальной цели: a IdxType<T, n> - это всего лишь T для любого n. Однако он позволяет это сделать:

template<typename T, size_t ... Indices>
void myFunc(IdxType<T, Indices> ... args);

Это здорово, так как это именно то, что вам нужно, чтобы получить вариационный набор идентично типизированных параметров. Единственная проблема, которая остается в том, что вы не можете делать такие вещи, как myFunc(obj1, obj2, obj3), поскольку компилятор не сможет вывести требуемый Indices - вам нужно будет сделать myFunc<1,2,3>(obj1, obj2, obj3), что является уродливым. К счастью, вы можете уйти от этого, обернув в вспомогательную функцию, которая заботится о генерации индекса для вас, используя make_index_sequence.

Ниже приведен полный пример, который похож на вашего посетителя (live demo здесь):

template<typename T, size_t sz>
using IdxType = T;

struct MyType
{};

struct Visitor
{
    void operator() (const MyType&) 
    {
        std::cout << "Visited" << std::endl;
    }
};

template <typename V>
void apply_visitor(std::index_sequence<>, V && v) 
{
}

template <typename V, typename T, size_t FirstIndex, size_t ... Indices>
void apply_visitor(std::index_sequence<FirstIndex, Indices...>, V && v, T && first, IdxType<T, Indices> && ... ss) {
    std::forward<V>(v)(std::forward<T>(first));
    apply_visitor(std::index_sequence<Indices...>(), std::forward<V>(v), std::forward<T>(ss) ...);
}

template <typename V, typename T, typename ... Rest>
void do_apply_visitor(V && v, T && t, Rest && ... rest )
{
    apply_visitor(std::make_index_sequence<sizeof...(Rest)+1>(), v, t, rest ... );
}

int main()
{
    Visitor v;

    do_apply_visitor(v, MyType{}, MyType{}, MyType{});

    return 0;
}

Ответ 4

Это принимает произвольный тип In и перемещает его ссылку r/lvalue на тип Out в неявном литье.

template<class Out>
struct forward_as {
  template<class In,
    std::enable_if_t<std::is_convertible<In&&,Out>{}&&!std::is_base_of<Out,In>{},int>* =nullptr
  >
  Out operator()(In&& in)const{ return std::forward<In>(in); }
  Out&& operator()(Out&& in)const{ return std::forward<Out>(in); }
  template<class In,
    std::enable_if_t<std::is_convertible<In&,Out&>{},int>* =nullptr
  >
  Out& operator()(In& in)const{ return in; }
  template<class In,
    std::enable_if_t<std::is_convertible<In const&,Out const&>{},int>* =nullptr
  >
  Out const& operator()(In const& in)const{ return in; }
};

При этом вот наш n-ary apply_visitor:

template <typename V, typename ... Ss,
  decltype(std::void_t<
    std::result_of_t<forward_as<my_struct>(Ss)>...
  >(),int())* =nullptr
>
void apply_visitor(V && v, Ss && ... ss) {
  auto convert = forward_as<my_struct>{};

  std::forward<V>(v)("foo", (convert(std::forward<Ss>(ss)).foo)...);
  std::forward<V>(v)("bar", (convert(std::forward<Ss>(ss)).bar)...);
  std::forward<V>(v)("baz", (convert(std::forward<Ss>(ss)).baz)...);
}

который не соответствует, если forward_as<my_struct> не может оцениваться на Ss.

живой пример

Ответ 5

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

В этом случае, почему бы не использовать std::initializer_list?

template <typename T>
void func(std::initializer_list<T> li) {
    for (auto ele : li) {
        // process ele
        cout << ele << endl;
    }
}

Как упоминалось в комментарии @Yakk, вы можете избежать копирования const. В этом случае вы можете скопировать указатели на std::initializer_list:

// Only accept pointer type
template <typename T>
void func(std::initializer_list<T> li) {
    for (auto ele : li) {
        // process pointers, so dereference first
        cout << *ele << endl;
    }
}

Или специализируйте func для указателей:

// Specialize for pointer
template <typename T>
void func(std::initializer_list<T*> li) {
    for (auto ele : li) {
        // process pointers, so dereference first
        cout << *ele << endl;
    }
}

my_struct a, b, c;
func({a, b, c}); // copies
func({&a, &b, &c}); // no copies, and you can change a, b, c in func

Ответ 6

В следующем примере можно использовать функцию времени компиляции, такую ​​как are_same:

#include <type_traits>

template<typename T, typename... O>
constexpr bool are_same() {
    bool b = true;
    int arr[] = { (b = b && std::is_same<T, O>::value, 0)... };
    return b;
}

int main() {
    static_assert(are_same<int, int, int>(), "!");
    static_assert(not are_same<int, double, int>(), "!");
}

Используйте его, как следует:

template <typename... Ts>
void func(Ts &&... ts) {
    static_assert(are_same<Ts...>(), "!");
    step_one(std::forward<Ts>(ts)...);
    step_two(std::forward<Ts>(ts)...);
}

У вас будет хорошее сообщение об ошибке времени компиляции в соответствии с запросом.

Ответ 7

Я думаю, вы можете сделать такую ​​функцию и проверить аргументы внутри своей функции.

template <typename T, typename... Args> bool check_args(T a, Args args)
{
static string type;
if(type == "") type = typeid(a).name;
else if(type != typeid(a).name) return false;
else return check_args(args...);
}
bool check_args() {return true;}