Пишу юниттесты для проверки студенческих семинарских работ.
(Чтобы не глазами ревьювить).
Одна из целей — это ловить ошибки, когда код компилируется, а не должен.
Например, когда студент написал класс, который неявно приводится к bool.
Подход номер раз: написать набор файлов, по одному на юниттест, и озадачить систему CI, чтобы удачей был код ошибки от компилятора.
Довольно муторное дело. И очень не наглядное.
Опять же, у студентов подход к работе такой: "ой, какие-то этапы интеграции не прошли, ну может быть, это я не полный комплект задач решил, сдам что есть, как есть..."
И они эти вещи легко игнорируют.
И главное, локально игнорируют.
(Кстати, как написать локальную цель для НЕ-сборки на симейке?)
Подход номер два: использовать метафункции наподобие std::is_convertible_v в static_assert.
// maybe<T> - студенческий класс
static_assert(std::is_constructible_v<bool, maybe<int>); // static_cast<bool>(mb)
static_assert(!std::is_convertible_v<maybe<int>, bool>); // bool v = mb
Хорошо, но это не юнит-тест. Это ультимативный тест. Если конкретно этот пункт не прошёл, то вообще ничего не скомпилировалось и не проверено.
Значит, нужно опять заводить кучу отдельных файлов.
Подход номер три: вытащить эти проверки в рантайм
EXPECT_TRUE(std::is_constructible_v<bool, maybe<int>); // static_cast<bool>(mb)
EXPECT_FALSE(std::is_convertible_v<maybe<int>, bool>); // bool v = mb
Хорошо, это юниттесты. Хотя с наглядностью тут пока что плоховато.
Следующий шаг: использовать условную компиляцию.
Она ещё и хороша для тех случаев, когда есть необязательные подзадачи. "Реализуйте такую-то фичу за плюс-столько-то баллов" или "реализуйте такую-то фичу обязательно, но подробности на ваше усмотрение".
Например, хочу сделать вот такую проверку
if constexpr(std::is_convertible_v<maybe<bool>, bool>) {
maybe<bool> mb = false;
bool b;
b = mb;
EXPECT_EQ(false, b) << "ахаха, нет! :trollface:"
} else {
// молодец, ибо нефиг писать ошибкоопасный код
}
И вот тут начинаются приколы компиляции!
Если в нешаблонном клиентском коде написать if constexpr, то компилятор, конечно, в ложную ветку управление не передаст, но попытку скомпилировать всё равно сделает. (Да, это так и задумано по стандарту! Хотя непонятно, зачем...)
Но есть такой хак, обмазать код шаблоном. Самое простое — это внутри юниттеста написать и вызвать полиморфную лямбду.
[](auto) {
.....
}(0);
Но это не спасёт, если условие if constexpr не зависит от параметра шаблона. Опять будет попытка компиляции обеих веток, как если бы там был просто if без constexpr.
Ещё больнее работает requires.
Казалось бы, он предназначен для обобщения SFINAE. Если какой-то стейтмент не удалось скомпилировать, значит, requires не выполнен.
Однако, если этот стейтмент нешаблонный, то будет не просто substitution failure, а именно что error прямо внутри requires.
maybe<bool> mb;
bool b;
void f(bool);
static_assert(!std::is_convertible_v<maybe<int>, bool>); // ок
static_assert(!requires { b = mb; }); // ошибка компиляции!
static_assert(!requires { f(mb); }); // ошибка компиляции!
Конечно, выход очевиден: сделать условия как-либо зависящими от шаблона.
[](auto fake) {
// так
using Bool = std::conditional_t<sizeof(fake)!=0, bool, void>;
// или вот так
struct LocalFuns { static void f(bool){} };
// или вот так
struct QQQ { using Bool = bool; }
using Bool = typename QQQ::Bool;
// или, наконец, вот так
[](auto& mb, auto& b) {
if constexpr(.....) { b = mb; }
}(mb, b);
}(0);
Но хотелось бы понять: такое энергичное поведение компилятора с if constexpr и requires — это так и задумано, или это какие-то неочевидности / недоделки / дефекты стандарта?
Я бегло потыкался и не нашёл ответа.
Здравствуйте, Кодт, Вы писали:
К>Но хотелось бы понять: такое энергичное поведение компилятора с if constexpr и requires — это так и задумано, или это какие-то неочевидности / недоделки / дефекты стандарта?
Ну вообще, написано, что неиспользуемая ветка должна быть отброшена (discarded):
https://timsong-cpp.github.io/cppwp/stmt.if#2
If the if statement is of the form if constexpr, the value of the condition is contextually converted to bool and the converted expression shall be a constant expression ([expr.const]); this form is called a constexpr if statement. If the value of the converted condition is false, the first substatement is a discarded statement, otherwise the second substatement, if present, is a discarded statement. During the instantiation of an enclosing templated entity ([temp.pre]), if the condition is not value-dependent after its instantiation, the discarded substatement (if any) is not instantiated. Each substatement of a constexpr if statement is a control-flow-limited statement
Там где-то еще говорится, что discarded ветви также не участвуют в выведении типа результата фунции, если тип результата объявлен с использованием auto.
Здравствуйте, reversecode, Вы писали:
R>я уже как то писал как обжегся с недоимплементированными стандартами в разных компилях
R>так что лучше взять последний кланг
R>https://github.com/llvm/llvm-project/releases
Поставил опыты на годболте со свежими тамошними msvc 19.38, clang 18.0.0 (и несвежими 16.0.*), gcc 14.0.0
https://gcc.godbolt.org/z/Td4GP5Tzf
| код |
| #include <type_traits>
#include <cstdio>
int main() {
struct M {};
static_assert(!std::is_convertible_v<M, bool>);
#if 0 // msvc, gcc, clang - compiler error
static_assert(!requires(M m, bool b) { b = m; });
#endif
struct Lambda { void operator()() const{
[[maybe_unused]] M m;
[[maybe_unused]] bool b;
if constexpr (std::is_convertible_v<M, bool>) {
#if 0
b = m;
char buf[-1];
static_assert(false);
#endif
} else printf("ok1 %d \n", __LINE__);
#if 0 // gcc, clang - compiler error
if constexpr (requires { b = m; }) {
b = m;
char buf[-1];
static_assert(false);
} else printf("ok2 %d \n", __LINE__);
#endif
} }; Lambda()();
[](){
[[maybe_unused]] M m;
[[maybe_unused]] bool b;
if constexpr (std::is_convertible_v<M, bool>) {
#ifdef _MSC_VER
b = m;
char buf[-1];
static_assert(false);
#endif
} else printf("ok3 %d \n", __LINE__);
#ifdef _MSC_VER // gcc, clang - compiler error
if constexpr (requires { b = m; }) {
b = m;
char buf[-1];
static_assert(false);
} else printf("ok4 %d \n", __LINE__);
#endif
}();
[](auto...) {
[[maybe_unused]] M m;
[[maybe_unused]] bool b;
if constexpr (std::is_convertible_v<M, bool>) {
#ifdef _MSC_VER // msvc: drop this {}; gcc, clang - compiler error
b = m;
char buf[-1];
#endif
// all compilers ignore this static assert (except clang < 18)
static_assert(false);
} else printf("ok5 %d \n", __LINE__);
#ifdef _MSC_VER // msvc: requires{}=false; gcc, clang - compiler error
if constexpr (requires { b = m; }) {
b = m;
char buf[-1];
} else printf("ok6 %d \n", __LINE__);
#endif
}();
[](auto...) {
struct T { using type = M; };
using TM = typename T::type; // mocked template dependency
[[maybe_unused]] TM m;
[[maybe_unused]] bool b;
if constexpr (std::is_convertible_v<TM, bool>) {
b = m;
#ifdef _MSC_VER
char buf[-1];
#else
static_assert(false);
#endif
} else printf("ok7 %d \n", __LINE__);
if constexpr (requires { b = m; }) {
b = m;
#ifdef _MSC_VER
char buf[-1];
#else
static_assert(false);
#endif
} else printf("ok8 %d \n", __LINE__);
}();
}
|
| |
Итак, что видно:
— если просто локальная функция — if constexpr работает как if, все компиляторы пытаются скомпилировать () и {}
— если простая лямбда — MSVC работает, как будто это шаблон, gcc и clang — как просто локальная функция
— если шаблонная лямбда и не зависящие от шаблона условия
— — MSVC пофигистично смотрит на содержимое {}, gcc и clang пытаются скомпилировать
— — все компиляторы трактуют ошибку в requires как ошибку
— если шаблонная лямбда и зависящие условия
— — ошибка в requires — это SFINAE
— — внутри {} msvc игнорирует нелепицу с отрицательным размером, зато даёт ошибку на static_assert; а gcc и clang — наоборот; зато все компиляторы съели невозможное действие.
Похоже, тут опять какой-то дефект стандарта? Раз компиляторы думают каждый в свою сторону?