Приведение в исполнение функции контракта во время компиляции, когда это возможно

(этот вопрос был вдохновлен тем, как я могу сгенерировать ошибку компиляции, чтобы предотвратить использование определенного VALUE (не типа) в функции?)

Допустим, у нас есть foo с одним аргументом, семантически определенный как

int foo(int arg) {
    int* parg;
    if (arg != 5) {
        parg = &arg;
    }

    return *parg;
}

Весь приведенный выше код используется для иллюстрации простой идеи - функция возвращает собственный аргумент, если аргумент не равен 5, в этом случае поведение не определено.

Теперь задача - изменить функцию таким образом, чтобы, если ее аргумент был известен во время компиляции, генерировалась диагностика компилятора (предупреждение или ошибка), а если нет, поведение оставалось неопределенным во время выполнения. Решение может зависеть от компилятора, если оно доступно в одном из 4 больших компиляторов.

Вот некоторые потенциальные маршруты, которые не решают проблему:

  • Делать функцию шаблоном, который принимает ее аргумент в качестве параметра шаблона - это не решает проблему, потому что делает функцию непригодной для аргументов времени выполнения
  • Создание функции constexpr - это не решает проблему, потому что даже когда компиляторы видят неопределенное поведение, они не производят диагностику в моих тестах - вместо этого, gcc вставляет инструкцию ud2, а это не то, что мне нужно.

Ответы

Ответ 1

Я получил ошибку с constexpr при использовании в константном выражении для:

constexpr int foo(int arg) {
    int* parg = nullptr;
    if (arg != 5) {
        parg = &arg;
    }
    return *parg;
}

демонстрация

Мы не можем знать, что значение аргумента известно при типе компиляции, но мы можем использовать тип, представляющий значение, с помощью std::integral_constant

// alias to shorten name. 
template <int N>
using int_c = std::integral_constant<int, N>;

Возможно, с UDL с operator "" _c будет 5_c, 42_c.

и затем добавьте перегрузку с этим:

template <int N>
constexpr auto foo(int_c<N>) {
    return int_c<foo(N)>{};
}

Так:

foo(int_c<42>{}); // OK
foo(int_c<5>{}); // Fail to compile

// and with previous constexpr:
foo(5); // Runtime error, No compile time diagnostic
constexpr auto r = foo(5); // Fail to compile

Как я уже сказал, аргументы, как известно, не являются постоянными внутри функции, и is_constexpr в стандарте не позволяет разрешить диспетчеризацию, но некоторые компиляторы предоставляют встроенные для этого (__builtin_constant_p), поэтому с MACRO мы можем выполнить диспетчеризацию:

#define FOO(X) [&](){ \
    if constexpr (__builtin_constant_p(X)) {\
        return foo(int_c<__builtin_constant_p (X) ? X : 0>{});\
    } else {\
        return foo(X); \
    } \
}()

демонстрация

Примечание. Невозможно использовать foo(int_c<X>{}) напрямую, даже если используется constexpr, так как все еще существует некоторая проверка синтаксиса.

Ответ 2

Компиляторы gcc/clang/intel поддерживают __builtin_constant_p, поэтому вы можете использовать что-то вроде этого:

template <int D>
int foo_ub(int arg) {
    static_assert(D != 5, "error");
    int* parg = nullptr;
    if (arg != 5) {
        parg = &arg;
    }

    return *parg;
}

#define foo(e) foo_ub< __builtin_constant_p(e) ? e : 0 >(e)

эти операторы приводят к ошибке времени компиляции:

  • foo(5)
  • foo(2+3)
  • constexpr int я = 5; foo(i);

в то время как все остальные - время выполнения segfault (или ub, если не используется nullptr)

Ответ 3

Он не идеален и требует использования аргументов в двух разных местах, но он "работает":

template<int N = 0>
int foo(int arg = 0) {
    static_assert(N != 5, "N cannot be 5!");
    int* parg;
    if (arg != 5) {
        parg = &arg;
    }

    return *parg;
}

Мы можем назвать это так:

foo<5>();   // does not compile
foo(5);     // UB
foo<5>(5);  // does not compile
foo<5>(10); // does not compile
foo<10>(5); // UB
foo();      // fine
foo<10>();  // fine
foo(10);    // fine