Используйте sfinae, чтобы выделить предпочтительный вариативный конструктор

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

#include <utility>
#include <iostream>

template <class Super>
struct Base : public Super {
  // 1
  template <typename... Args,
            typename = std::enable_if_t<std::is_constructible_v<Super, Args&&...>>>
  explicit Base(unsigned long count, Args&&... args)
    : Super(std::forward<Args>(args)...), count(count) {}

  // 2
  template <typename... Args,
            typename = std::enable_if_t<std::is_constructible_v<Super, Args&&...>>>
  explicit Base(Args&&... args) : Super(std::forward<Args>(args)...), count(0) {}

  unsigned long count;
};

struct A {
  explicit A(unsigned long id) {}
  A() {}
};

struct B {
  explicit B(const char* cstring) {}
  explicit B(unsigned long id, const char* cstring) {}
  explicit B(unsigned long id, A a) {}
  B() {}
};

int main() {
  auto a1 = Base<A>(7);             // selects 2, but I want 1
  auto a2 = Base<A>(7ul);           // selects 1
  auto a3 = Base<A>(7, 10);         // selects 1
  auto b1 = Base<B>(4);             // selects 1
  auto b2 = Base<B>("0440");        // selects 2
  auto b3 = Base<B>(4, "0440");     // selects 2, but I want 1
  auto b4 = Base<B>(4, 4, "0440");  // selects 1
  auto b5 = Base<B>(4, A());        // selects 2
  std::printf("%lu %lu %lu\n", a1.count, a2.count, a3.count);
  std::printf("%lu %lu %lu %lu %lu\n", b1.count, b2.count, b3.count, b4.count, b5.count);
  return 0;
}

Вывод 0 7 7 в первой строке, но я хочу 7 7 7, т.е. Base<A>(7) должен выбирать первый конструктор, а не второй. То же самое для b3.

sfinae для конструкторов позволяет компилятору выбирать 1, когда 2 не соответствует аргументам, но я хочу, чтобы он выбирал конструктор 1 каждый раз, когда он совпадает. В случае a1 неявное преобразование 7 из int в unsigned long вынуждает выбирать конструктор 2, почему я тоже не понимаю.

Как мне это решить?


person elbrunovsky    schedule 28.05.2019    source источник
comment
Я думаю, что ответ Якка здесь, вероятно, вам нужен. Вероятно, достаточно, чтобы его можно было считать обманом?   -  person Barry    schedule 29.05.2019
comment
@Barry действительно выглядит правильным решением, посмотрю на него, спасибо!   -  person elbrunovsky    schedule 29.05.2019
comment
@Barry Это правильное решение, если первый аргумент, преобразуемый в unsigned long, означает, что он не может быть первым аргументом в Super.   -  person Deduplicator    schedule 29.05.2019


Ответы (2)


Соберем требования:

  1. Первый аргумент неявно преобразуется в unsigned long, остальные могут построить базу => сделать это.
  2. Не подходит 1, и аргументы могут построить базу => сделать это.
struct Base : Super {
    // This one should be preferred:
    template <class... Args, class = std::enable_if_t<std::is_constructible_v<Super, Args...>>>
    explicit Base(unsigned long count = 0, Args&&... args)
    : Super(std::forward<Args>(args))
    , count(count) {
    }

    // Only if the first is non-viable:
    template <class U, class... Args, class = std::enable_if_t<
        !(std::is_convertible_v<U, unsigned long> && std::is_constructible_v<Super, Args...>)
        && std::is_constructible_v<Super, U, Args>>>
    explicit Base(U&& u, Args&&... args)
    : Base(0, std::forward<U>(u), std::forward<Args>(args)...) {
    }
    unsigned long count;
};

Имейте в виду, что оба шаблонных ctor являются кандидатами на неявные преобразования. Помещение explicit там, где это необходимо, оставлено в качестве упражнения для читателя.

Если бы можно было рассмотреть больше альтернатив, было бы целесообразно использовать диспетчеризацию тегов:

template <std::size_t N> struct priority : priority<N - 1> {};
template <> struct priority<0> {};

template <class... Ts>
static constexpr bool has_priority_v = (std::is_base_of_v<priority<0>, std::decay_t<Ts>> || ...);

class Base : Super {
    template <class UL, class... Ts, class = std::enable_if_t<
        std::is_convertible_v<UL, unsigned long> && std::is_constructible_v<Super, Ts...>>>
    Base(priority<1>, UL&& count, Ts&&... ts)
    : Super(std::forward<Ts>(ts)...), count(count)
    {}

    template <class... Ts, class = std::enable_if_t<std::is_constructible_v<Super, Ts...>>>
    Base(priority<0>, Ts&&... ts)
    : Base(priority<1>(), std::forward<Ts>(ts)...)
    {}
public:
    template <class... Ts, class = std::enable_if_t<
        !has_priority<Ts...> && std::is_constructible_v<Base, priority<>, Ts...>>>
    explicit Base(Ts&&... ts)
    : Base(priority<1>(), std::forward<Ts>(ts)...)
    {}
person Deduplicator    schedule 28.05.2019

Кажется, это делает то, что вы хотите (хотя комментарии ниже указывают на то, что это может быть недопустимой конструкцией):

struct Base : public Super {
  // 1
  template <typename... Args,
            typename = std::enable_if_t<std::is_constructible_v<Super, Args...>>>
  explicit Base(unsigned long count, Args&&... args)
    : Super(std::forward<Args&&>(args)...), count(count) {
        static_assert(std::is_same_v<Super*, void>);
    }

  // 2
  template <typename... Args,
            typename = std::enable_if_t<!std::is_constructible_v<Base, Args...>>>
  explicit Base(Args&&... args) : Super(std::forward<Args>(args)...), count(0) {}

  unsigned long count;
};

Мне нравится, как Godbolt теперь запускает ваш код!

https://godbolt.org/z/UEweak

person xaxxon    schedule 28.05.2019
comment
Разве std::enable_if_t<!std::is_constructible_v<Base, Args...>> не читается как enable только в том случае, если он не может быть создан из предоставленных типов? Разве это не парадокс? - person NathanOliver; 29.05.2019
comment
Это, похоже, указывает на его неправильный формат, отчет о недоставке - person NathanOliver; 29.05.2019
comment
Хорошо, я бы в это поверил, но он работает со всеми тремя основными компиляторами. У меня нет дальнейших комментариев по поводу качества моего решения и я не претендую на то, что оно хорошо. Отредактированный «ответ», чтобы указать на возможные недостатки. - person xaxxon; 29.05.2019