Могут ли констевальные функции из разных единиц перевода мешать?
Я пытаюсь вникнуть в последствия того, что функция inline
и наткнулся на этот вопрос. Рассмотрим эту небольшую программу (демонстрацию):
/* ---------- main.cpp ---------- */
void other();
constexpr int get()
{
return 3;
}
int main()
{
std::cout << get() << std::endl;
other();
}
/* ---------- other.cpp ---------- */
constexpr int get()
{
return 4;
}
void other()
{
std::cout << get() << std::endl;
}
При компиляции без оптимизации программа выдает следующий результат:
3
3
Возможно, это не то, что мы хотим, но, по крайней мере, я могу это объяснить.
- Компилятору не требуется вычислять результаты
constexpr
функции во время компиляции, поэтому было решено отложить его на время выполнения. constexpr
по функциям подразумеваетinline
- Наши
get()
функции имели разные реализации - Мы не декларировали
get()
функции статические - Компоновщик должен выбрать только одну реализацию
get()
функция
Так получилось, что компоновщик выбрал get()
от main.cpp
, который вернул 3.
Теперь о части, которую я не понимаю. Я просто изменилget()
функции от constexpr
к consteval
. Теперь компилятор должен вычислить значение во время компиляции, то есть до времени компоновки (верно?). Я ожидалget()
функции вообще не присутствовать в объектных файлах.
Но когда я запускаю его (демонстрацию), у меня получается точно такой же результат! Как такое может быть?.. То есть да, я понимаю, что это неопределенное поведение, но суть не в этом. Почему значения, которые должны были быть вычислены во время компиляции, мешали другой единице перевода?
UPD: Мне известно, что эта функция указана в clang как нереализованная, но вопрос в любом случае применим. Разрешено ли совместимому компилятору демонстрировать такое поведение?
3 ответа
Программа с двумя определениями одной и той же встроенной функции - это плохо сформированная программа, не требующая диагностики.
Стандарт не предъявляет требований к поведению плохо сформированной программы во время выполнения или компиляции.
Теперь в C++ нет "времени компиляции", как вы это себе представляете. Хотя почти каждая реализация C++ компилирует файлы, связывает их, строит двоичный файл, а затем запускает его, стандартные советы C++ в этом отношении.
В нем говорится о единицах перевода и о том, что происходит, когда вы объединяете их в программу, и о том, каково поведение этой программы во время выполнения.
...
На практике ваш компилятор мог бы построить карту от символа до некоторой внутренней структуры. Он компилирует ваш первый файл, а затем во втором файле он все еще обращается к этой карте. Новое определение той же встроенной функции? Просто пропустите это.
Во-вторых, ваш код должен создавать выражение константы времени компиляции. Но выражение константы времени компиляции не является наблюдаемым свойством в контексте, в котором вы его использовали, и нет никаких побочных эффектов для его выполнения при ссылке или даже во время выполнения! И под "как-будто" этому ничего не мешает.
consteval
говорит: "Если я запустил это и правила, которые позволяют ему быть постоянным выражением, нарушены, я должен ошибиться, а не прибегать к непостоянному выражению". Это похоже на "он должен запускаться во время компиляции", но это не то же самое.
Чтобы определить, что из этого происходит, попробуйте следующее:
template<auto x>
constexpr std::integral_constant< decltype(x), x > constant = {};
теперь замените строки печати на:
std::cout << constant<get()> << std::endl;
это делает непрактичным откладывание оценки на время выполнения / связывания.
Это будет различать "умный компилятор" и "кэширование" get
компилятор "from" оценивает его позже во время компоновки ", поскольку определяет, какой ostream& <<
для вызова требуется создание экземпляра типа constant<get()>
, что, в свою очередь, требует оценки get()
.
Компиляторы обычно не откладывают разрешение перегрузки на время компоновки.
Требование наличия consteval
Функция состоит в том, что каждый вызов к нему должен приводить к постоянному выражению.
Как только компилятор убедится, что вызов действительно производит постоянное выражение, нет требования, что он не должен кодировать функцию и вызывать ее во время выполнения. Конечно, для некоторыхconsteval
функций (например, предусмотренных для отражения) лучше этого не делать (по крайней мере, если он не хочет помещать все свои внутренние структуры данных в объектный файл), но это не общее требование.
Неопределенное поведение не определено.
Ответ заключается в том, что это все еще нарушение ODR, независимо от того, является ли функция constexpr
или consteval
. Возможно, с конкретным компилятором и конкретным кодом вы можете получить ожидаемый ответ, но он все еще плохо сформирован, и диагностики не требуется.
Что вы могли сделать, так это определить их в анонимных пространствах имен:
/* ---------- main.cpp ---------- */
void other();
namespace {
constexpr int get()
{
return 3;
}
}
int main()
{
std::cout << get() << std::endl;
other();
}
/* ---------- other.cpp ---------- */
namespace {
constexpr int get()
{
return 4;
}
}
void other()
{
std::cout << get() << std::endl;
}
Но еще лучше просто использовать модули:
/* ---------- main.cpp ---------- */
import other;
constexpr int get()
{
return 3;
}
int main()
{
std::cout << get() << std::endl; // print 3
other();
}
/* ---------- other.cpp ---------- */
export module other;
constexpr int get() // okay, module linkage
{
return 4;
}
export void other()
{
std::cout << get() << std::endl; // print 4
}