static_assert перед списком инициализатора конструктора

Существует не шаблонный класс, который имеет шаблонный конструктор. Можно ли проверить статическое утверждение перед инициализацией переменных-членов в таком конструкторе?

Например, следующий код выполняется T::value() перед проверкой T есть такой метод.

class MyClass
{
public:
    template<typename T>
    MyClass(const T &t)
        : m_value(t.value())
    {
        static_assert(HasValueMethod<T>::value, "T must have a value() method");
    }

private:
    int m_value;
};

размещение static_assert в теле конструктора работает нормально, за исключением того, что он печатает "метод T должен иметь значение ()" в самом конце, после всех сообщений об ошибках из списка инициализатора члена, например:

prog.cpp: In instantiation of ‘MyClass::MyClass(const T&) [with T = int]’:
prog.cpp:24:16:   required from here
prog.cpp:12:21: error: request for member ‘value’ in ‘t’, which is of non-class type ‘const int’
         : m_value(t.value())
                   ~~^~~~~
prog.cpp:14:9: error: static assertion failed: T must have a value() method
         static_assert(HasValueMethod<T>::value, "T must have a value() method");
         ^~~~~~~~~~~~~

Я нахожу это немного сбивающим с толку и задаюсь вопросом, можно ли напечатать "T должен иметь метод value()" перед попыткой инициализировать переменные-члены.

Я знаю, что я мог бы использовать enable_if и SFINAE отключить этот конструктор для неуместного Tс, но я хотел бы сказать пользователю что-то более значимое, чем "метод не найден".

3 ответа

Ты можешь использовать std::enable_if СФИНАЕ из конструктора, который делает static_assert в зависимости от того T имеет функцию-член value()сохраняя реальную реализацию отделенной.

Первый конструктор выбирается, если T имеет value() метод, и реализуется как обычно (за исключением того, что он нуждается в std::enable_if для того, чтобы быть выбранным):

template <typename T, typename = std::enable_if_t<HasValueMethod<T>::value>>
MyClass(const T &t) : m_value(t.value())
{}

Поэтому нам нужно, чтобы второй конструктор был SFINAEd из-за перегрузки функций, так как первый уже знает, что T::value существует:

template <typename T, typename = std::enable_if_t<!HasValueMethod<T>::value>>
MyClass(const T &, ...)
{
  static_assert(HasValueMethod<T>::value, "T must have a value() method");
}

Обратите внимание на переменный параметр ...: это необходимо для того, чтобы дифференцировать прототип конструктора, чтобы он не сталкивался с первым (они должны отличаться, иначе неоднозначные прототипы приводят к ошибке компиляции). Вы ничего не передадите ему, он просто создан, чтобы сделать его другим прототипом.

Обратите внимание, что предикат для std::enable_if то же самое, но отрицается. когда HasValueMethod<T>::value Значение false, первый конструктор является SFINAEd из-за перегрузки функции, но не второй, который затем вызовет статическое утверждение.

Вам все еще нужно использовать HasValueMethod<T>::value в параметре статического assert, так что это зависит от T быть выполненным. В противном случае, просто положить false он всегда будет срабатывать независимо от того, выбран ли он.

Вот что печатает GCC, когда T не имеет .value():

main.cpp: In instantiation of 'MyClass::MyClass(const T&, ...) [with T = A; <template-parameter-1-2> = void]':
main.cpp:35:18:   required from here
main.cpp:21:9: error: static assertion failed: T must have a value() method
         static_assert(HasValueMethod<T>::value, "T must have a value() method");

         ^~~~~~~~~~~~~

Вот Кланг:

main.cpp:21:9: error: static_assert failed "T must have a value() method"
        static_assert(HasValueMethod<T>::value, "T must have a value() method");
        ^    

В целом, есть проблема (как отмечено @TC в комментариях) с этим подходом: MyClass теперь конвертируемо из чего угодно с точки зрения неоцененных контекстов. То есть,

static_assert(std::is_convertible_v</*anything*/, MyClass>); // Always true.

В C++20, когда, как мы надеемся, есть концепции, это легко решается с помощью requires пункт:

template <typename T>
  requires HasValueMethod<T>::value
MyClass(const T &t) : m_value(t.value())
{}

Вы могли бы прямо выразить HasValueMethod<T> в requires пункт так же хорошо:

template <typename T>
  requires requires (T a) { { a.value() } -> int; }
MyClass(const T &t) : m_value(t.value())
{}

Или преобразует HasValueMethod<T> в реальную концепцию:

template <typename T>
concept HasValueMethod = requires (T a) {
    { a.value() } -> int;
};

// Inside `class MyClass`.
template <typename T>
  requires HasValueMethod<T>
MyClass(const T &t) : m_value(t.value())
{}

Такие решения делают std::is_convertible_v<T, MyClass> работать как положено.

Принеси static_assert() ближе к использованию. В этом случае вспомогательная функция сделает это:

class MyClass
{
    template<typename T>
    static int get_value(const T& t)
    {
        static_assert(HasValueMethod<T>::value, "T must have a value() method");
        return t.value();
    }

public:
    template<typename T>
    MyClass(const T &t)
        : m_value(get_value(t))
    {
    }

private:
    int m_value;
};

Это не только исправляет порядок сообщений об ошибках, но и позволяет повторно использовать сообщение для каждого пути, который требует value() функция-член.

Если вы не планируете ограничивать конструктор SFINAE и всегда хотите, чтобы при возникновении ошибки HasValueMethod ложь, вы можете просто написать "сложный" вариант вашего класса черты:

template<class T>
struct AssertValueMethod
{
  static_assert(HasValueMethod<T>::value, "T must have a value() method");
  using type = void; // note: needed to ensure instantiation, see below ...
};

template< typename T, typename = typename AssertValueMethod<T>::type >
MyClass(const T &t): ...

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

Другие вопросы по тегам