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

(Im) совершенная переадресация с вариативными шаблонами

Сводка

Учитывая тип с конструктором конструкторов Variadic, который перенаправляет аргументы в класс реализации, можно ли ограничивать типы, пересылаемые с помощью SFINAE?

Подробнее

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

struct foo
{
  foo() = default;

  foo(foo const&) 
  {
      std::cout << "copy" << std::endl;
  }

  template <
    typename T,
    typename Dummy = typename std::enable_if<
      !std::is_same<
          T,
          typename std::add_lvalue_reference<foo>::type
      >::value
    >::type
  >
  foo(T&& x)
    : impl(std::forward<T>(x))
  {
      std::cout << "uref" << std::endl;
  }

  foo_impl impl;
};

Это ограничение универсальной ссылки полезно, потому что в противном случае класс реализации получит неконстантный ссылочный номер типа foo, о котором он не знает. Полный пример в LWS.

Вопрос

Но как это работает с вариативными шаблонами? Это вообще возможно? Если да, то как? Наивное расширение не работает:

template <
  typename... Args,
  typename Dummy = typename std::enable_if<
    !std::is_same<
        Args...,
        typename std::add_lvalue_reference<foo>::type
    >::value
  >::type
>
foo(Args&&... args)
  : impl(std::forward<Args>(args)...)
{
    std::cout << "uref" << std::endl;
}

(Также в LWS.)

EDIT: Я обнаружил, что Р. Мартиньо Фернандес писал о вариации этой проблемы в 2012 году: http://flamingdangerzone.com/cxx11/2012/06/05/is_related.html

4b9b3361

Ответ 1

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

Эта конкретная форма EnableIf будет использоваться, но это детализация реализации, которая не изменяет сущность описанных здесь методов, Он также предположил, что существуют алиасы And и Not для объединения разных метакомплексов. Например. And<std::is_integral<T>, Not<is_const<T>>> более удобно, чем std::integral_constant<bool, std::is_integral<T>::value && !is_const<T>::value>.

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

Ограничьте себя

template<typename T>
using Unqualified = typename std::remove_cv<
    typename std::remove_reference<T>::type
>::type;

struct foo {
    template<
        typename... Args
        , EnableIf<
            Not<std::is_same<foo, Unqualified<Args>>...>
        >...
    >
    foo(Args&&... args);
};

Преимущество: избегает конструктору от участия в разрешении перегрузки в следующем сценарии:

foo f;
foo g = f; // typical copy constructor taking foo const& is not preferred!

Недостаток: участвует во всех других разрешениях перегрузки

Ограничить конструктивное выражение

Так как конструктор имеет моральные последствия построения a foo_impl из Args, представляется естественным выражать ограничения на эти точные слагаемые:

    template<
        typename... Args
        , EnableIf<
            std::is_constructible<foo_impl, Args...>
        >...
    >
    foo(Args&&... args);

Преимущество: Теперь это официально ограниченный шаблон, поскольку он участвует только в разрешении перегрузки, если выполняется какое-либо семантическое условие.

Недостаток: Имеет ли значение следующее:

// function declaration
void fun(foo f);
fun(42);

Если, например, foo_impl есть std::vector<double>, то да, код действителен. Поскольку std::vector<double> v(42); - это допустимый способ построения вектора такого типа, то он может быть преобразован из int в foo. Другими словами, std::is_convertible<T, foo>::value == std::is_constructible<foo_impl, T>::value, отложив в сторону вопрос о других конструкторах для foo (помните об измененном порядке параметров - это несчастливо).

Ограничить конструктивное выражение, явно

Естественно, сразу приходит в голову следующее:

    template<
        typename... Args
        , EnableIf<
            std::is_constructible<foo_impl, Args...>
        >...
    >
    explicit foo(Args&&... args);

Вторая попытка, которая отмечает конструктор explicit.

Преимущество: Избегает вышеуказанного недостатка! И это не займет много времени - пока вы не забудете, что explicit.

Недостатки: Если foo_impl есть std::string, то может быть неудобно следующее:

void fun(foo f);
// No:
// fun("hello");
fun(foo { "hello" });

Это зависит от того, является ли foo, например, тонкой оболочкой вокруг foo_impl. Вот что я считаю более раздражающим недостатком, предполагая, что foo_impl есть std::pair<int, double*>.

foo make_foo()
{
    // No:
    // return { 42, nullptr };
    return foo { 42, nullptr };
}

Мне не кажется, что explicit фактически спасает меня от чего-либо здесь: в фигурных скобках есть два аргумента, поэтому, очевидно, это не преобразование, а тип foo уже присутствует в сигнатуре, поэтому мне бы хотелось избавиться от него, когда я чувствую, что он избыточен. std::tuple страдает от этой проблемы (хотя такие фабрики, как std::make_tuple, немного облегчают боль).

Отдельно ограничивать преобразование из конструкции

Отдельно выражаем конструктивные и конверсионные ограничения:

// New trait that describes e.g.
// []() -> T { return { std::declval<Args>()... }; }
template<typename T, typename... Args>
struct is_perfectly_convertible_from: std::is_constructible<T, Args...> {};

template<typename T, typename U>
struct is_perfectly_convertible_from: std::is_convertible<U, T> {};

// New constructible trait that will take care that as a constraint it
// doesn't overlap with the trait above for the purposes of SFINAE
template<typename T, typename U>
struct is_perfectly_constructible
: And<
    std::is_constructible<T, U>
    , Not<std::is_convertible<U, T>>
> {};

Использование:

struct foo {
    // General constructor
    template<
        typename... Args
        , EnableIf< is_perfectly_convertible_from<foo_impl, Args...> >...
    >
    foo(Args&&... args);

    // Special unary, non-convertible case
    template<
        typename Arg
        , EnableIf< is_perfectly_constructible<foo_impl, Arg> >...
    >
    explicit foo(Arg&& arg);
};

Преимущество: Конструкция и преобразование foo_impl теперь являются необходимыми и достаточными условиями для построения и преобразования foo. Другими словами, std::is_convertible<T, foo>::value == std::is_convertible<T, foo_impl>::value и std::is_constructible<foo, Ts...>::value == std::is_constructible<foo_impl, T>::value сохраняются (почти).

Недостаток? foo f { 0, 1, 2, 3, 4 }; не работает, если foo_impl есть, например. std::vector<int>, поскольку ограничение связано с построением стиля std::vector<int> v(0, 1, 2, 3, 4);. Можно добавить еще одну перегрузку с помощью std::initializer_list<T>, которая ограничена на std::is_convertible<std::initializer_list<T>, foo_impl> (слева как упражнение для читателя) или даже перегрузка, принимающая std::initializer_list<T>, Ts&&... (ограничение также оставлено в качестве упражнения для читателя), но помните, что "преобразование" из более чем одного аргумента не является конструкцией!). Обратите внимание, что нам не нужно изменять is_perfectly_convertible_from, чтобы избежать совпадения.

Более угодливые среди нас также будут стараться различать узкие преобразования против других преобразований.

Ответ 2

Вы можете поместить Args внутри более сложных выражений и развернуть это как expression(Args).... Поэтому

!std::is_same<Args, typename std::add_lvalue_reference<foo>::type>::value...

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

template<bool... Args> struct and_;

template<bool A, bool... Args>
struct and_<A, Args...>{
  static constexpr bool value = A && and_<Args...>::value;
};
template<bool A>
struct and_<A>{
  static constexpr bool value = A;
};

//...
template <typename... Args,
          typename Dummy = typename std::enable_if<
              and_<!std::is_same<Args, 
                                 typename std::add_lvalue_reference<foo>::type
                                >::value...>::value
              >::type
         >
foo(Args&&... args) : impl(std::forward<Args>(args)...)
{
  std::cout << "uref" << std::endl;
}

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