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

Начиная с C++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 - я должен попытаться найти общую базу для всех параметров, я думаю?

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


person Chris Beck    schedule 22.07.2016    source источник
comment
Вы хотите, чтобы все они были определенного известного типа или просто одного и того же неизвестного типа?   -  person Quentin    schedule 22.07.2016
comment
@cpplearner: О, это довольно мило, я не знал об этом   -  person Chris Beck    schedule 22.07.2016
comment
@Quentin: Думаю, если бы это имело значение, я бы хотел увидеть пример. Я хотел бы знать хороший способ сделать более общий - того же неизвестного типа   -  person Chris Beck    schedule 22.07.2016
comment
Нет смысла forward<V>(v) более одного раза.   -  person aschepler    schedule 14.08.2016
comment
@aschepler: Интересно, но я думаю, что все в порядке. Мы перенаправляем вызов метода, а не перемещаем посетителя. Единственное, что это делает, это включает && ref-qualified operator(). Поэтому я думаю, что проблема может возникнуть только в том случае, если operator() решит вызвать delete this из-за квалификаторов && или чего-то подобного. Я, конечно, хочу переслать V в отношении const. Поэтому я думаю, что повторное перенаправление V здесь нормально. Вы можете попытаться убедить меня в обратном.   -  person Chris Beck    schedule 14.08.2016


Ответы (7)


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

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

}

Однако это будет гарантировано только для SFINAE, начиная с C++17. Clang и GCC уже реализуют это таким образом.

person Quentin    schedule 22.07.2016
comment
@JohanLundberg Я поместил его туда, чтобы эмулировать передачу параметров, но после некоторого тестирования он фактически нарушает передачу по ссылке. Исправлена. - person Quentin; 23.07.2016
comment
common_type определяет, существует ли общий тип, в который все остальные могут быть неявно преобразованы. Это не значит, что все они одного типа. - person skypjack; 29.07.2016
comment
@skypjack последний абзац в OP указывает на то, что это желаемое поведение. - person Quentin; 29.07.2016

Вот черта типа, которую вы можете использовать в 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{}; не компилируется).

person SirGuy    schedule 22.07.2016

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

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

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

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

Вышеупомянутое не имеет реальной цели: 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.

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

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;
}
person Smeeheey    schedule 22.07.2016
comment
Хороший подход, я даже не думал так делать - person Chris Beck; 22.07.2016

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

В таком случае, почему бы не использовать 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
person for_stack    schedule 22.07.2016
comment
Ограничено const копиями данных. - person Yakk - Adam Nevraumont; 22.07.2016

Это берет произвольный тип 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-арный 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.

живой пример

person Yakk - Adam Nevraumont    schedule 22.07.2016
comment
Я не думал, что вы можете построить void_t, так как это просто псевдоним для void - person SirGuy; 22.07.2016
comment
@GuyGreer void() действителен. Не уверен, что void{} есть, поэтому удалил его. Теперь с живым примером, так что он нравится как минимум 2 компиляторам. - person Yakk - Adam Nevraumont; 22.07.2016

Возможным решением было бы использование функции времени компиляции, такой как 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)...);
}

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

person skypjack    schedule 28.07.2016

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

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;}
person Zeta    schedule 22.07.2016
comment
Это не позволит зарегистрировать static_assert или отключить перегрузку с enable_if - person SirGuy; 22.07.2016