проблемы ленивой компиляции
От: Кодт Россия  
Дата: 25.12.23 12:57
Оценка: +1
Пишу юниттесты для проверки студенческих семинарских работ.
(Чтобы не глазами ревьювить).

Одна из целей — это ловить ошибки, когда код компилируется, а не должен.
Например, когда студент написал класс, который неявно приводится к 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 — это так и задумано, или это какие-то неочевидности / недоделки / дефекты стандарта?
Я бегло потыкался и не нашёл ответа.
Перекуём баги на фичи!
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.