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 выбранную перегрузку, вы всегда можете написать правильный конструктор делегирования без изменения логики статического утверждения...