Могут ли констевальные функции из разных единиц перевода мешать?

Я пытаюсь вникнуть в последствия того, что функция 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

Возможно, это не то, что мы хотим, но, по крайней мере, я могу это объяснить.

  1. Компилятору не требуется вычислять результаты constexpr функции во время компиляции, поэтому было решено отложить его на время выполнения.
  2. constexpr по функциям подразумевает inline
  3. Наши get() функции имели разные реализации
  4. Мы не декларировали get() функции статические
  5. Компоновщик должен выбрать только одну реализацию 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
}
Другие вопросы по тегам