Constexpr и инициализация статического константного указателя void с реинтерпретом, который компилятор прав?

Рассмотрим следующий фрагмент кода:

struct foo {
  static constexpr const void* ptr = reinterpret_cast<const void*>(0x1);
};

auto main() -> int {
  return 0;
}

Вышеприведенный пример компилируется в g++ v4.9 (Live Demo), в то время как он не компилируется в clang v3.4 (Live Demo) и генерирует следующую ошибку:

error: constexpr variable 'ptr' должна быть инициализирована константным выражением

Вопросы:

  • Какой из двух компиляторов прав в соответствии со стандартом?

  • Каков правильный способ объявления выражения такого рода?

Ответ 1

TL; DR

clang правильно, это известная ошибка gcc. Вы можете либо использовать intptr_t вместо этого, и приводить, когда вам нужно использовать значение, или если это не работает, тогда и gcc, и clang поддерживают небольшой документированный обходной путь, который должен позволить ваш конкретный вариант использования.

Подробнее

Таким образом, clang верен для этого, если мы перейдем к разделу черновика стандарта С++ 11 5.19 В параграфе 2 константных выражений сказано:

Условное выражение является основным константным выражением, если оно включает в себя одно из следующего в качестве потенциально оцениваемого подвыражения [...]

и включает в себя следующую маркировку:

— a reinterpret_cast (5.2.10);

Одним из простых решений было бы использовать intptr_t:

static constexpr intptr_t ptr = 0x1;

а затем приведите его позже, когда вам нужно его использовать:

reinterpret_cast<void*>(foo::ptr) ;

Это может быть заманчиво, но эта история становится более интересной. Это известная и все еще открытая ошибка gcc, см. Ошибка 49171: [С++ 0x] [constexpr] Выражения констант поддерживают reinterpret_cast. Из обсуждения ясно, что разработчики gcc имеют несколько ясных вариантов использования для этого:

Я считаю, что нашел соответствующее использование reinterpret_cast в константе выражения, используемые в С++ 03:

//---------------- struct X {  X* operator&(); };

X x[2];

const bool p = (reinterpret_cast<X*>(&reinterpret_cast<char&>(x[1]))
- reinterpret_cast<X*>(&reinterpret_cast<char&>(x[0]))) == sizeof(X);

enum E { e = p }; // e should have a value equal to 1
//----------------

В основном эта программа демонстрирует технику, библиотеку С++ 11 Функция addressof основана на и, таким образом, исключает reinterpret_cast безусловно, из константных выражений в базовом языке сделает эту полезную программу недействительной и сделает невозможным объявите addressof как функцию constexpr.

но не удалось получить исключение для этих случаев использования, см. закрытые номера 1384:

Хотя reinterpret_cast был разрешен в адресной константе Выражения в С++ 03, это ограничение было реализовано в некоторых компиляторы и не доказали, чтобы сломать значительные объемы кода. РГС Считается, что осложнения работы с указателями, чьи типы изменено (арифметика указателя и разыменование не могут быть разрешены на такие указатели) перевесил возможную полезность расслабления тока ограничение.

НО, очевидно, gcc и clang поддерживают небольшое документированное расширение, которое позволяет постоянно сворачивать непостоянные выражения с использованием __builtin_constant_p (exp), и поэтому следующие выражения принимаются как gcc, так и clang :

static constexpr const void* ptr = 
  __builtin_constant_p( reinterpret_cast<const void*>(0x1) ) ? 
    reinterpret_cast<const void*>(0x1) : reinterpret_cast<const void*>(0x1)  ;

Найти документацию по этому вопросу практически невозможно, но эта комманда llvm является информативной, и следующие фрагменты обеспечивают интересное чтение:

support the gcc __builtin_constant_p()?... :... folding hack in C++11

и:

// __builtin_constant_p ? : is magical, and is always a potential constant.

и:

// This macro forces its argument to be constant-folded, even if it not
// otherwise a constant expression.
#define fold(x) (__builtin_constant_p(x) ? (x) : (x))

Мы можем найти более формальное объяснение этой функции в электронном письме gcc-patches: константы C, VLA и т.д. Исправляют, где говорится:

Кроме того, правила для вызовов __builtin_constant_p как условные условия выражения в реализации более смягчены, чем те, в формальной модели: выделенная половина условного выражения полностью складывается независимо от того, является ли она формальной константой выражением, поскольку __builtin_constant_p проверяет полностью сложенный аргумент сам.

Ответ 2

Clang прав. Результат повторного интерпретации никогда не является константным выражением (см. с++ 11 5.19/2).

Цель константных выражений состоит в том, что они могут быть аргументированы как значения, а значения должны быть действительными. То, что вы пишете, не является допустимым указателем (поскольку это не адрес объекта или не связан с адресом объекта по арифметике указателей), поэтому вы не можете использовать его как константное выражение. Если вы просто хотите сохранить номер 1, сохраните его как uintptr_t и выполните переинтерпретацию на сайте использования.


Кроме того, чтобы немного углубиться в понятие "действительных указателей", рассмотрим следующие указатели constexpr:

constexpr int const a[10] = { 1 };
constexpr int * p1 = a + 5;

constexpr int const b[10] = { 2 };
constexpr int const * p2 = b + 10;

// constexpr int const * p3 = b + 11;    // Error, not a constant expression

static_assert(*p1 == 0, "");             // OK

// static_assert(p1[5] == 0, "");        // Error, not a constant expression

static_assert(p2[-2] == 0, "");          // OK

// static_assert(p2[1] == 0, "");        // Error, "p2[1]" would have UB

static_assert(p2 != nullptr, "");        // OK

// static_assert(p2 + 1 != nullptr, ""); // Error, "p2 + 1" would have UB

И p1, и p2 являются константными выражениями. Но является ли результат арифметики указателя константным выражением, зависит от того, является ли он UB! Такое рассуждение было бы по существу невозможным, если бы вы позволили значениям reinterpret_casts быть постоянными выражениями.

Ответ 3

Я также сталкивался с этой проблемой при программировании микроконтроллеров AVR. Avr-libc имеет заголовочные файлы (включенные через <avr/io.h>, которые делают макет регистров для каждого микроконтроллера определением макросов , таких как:

#define TCNT1 (*(volatile uint16_t *)(0x84))

Это позволяет использовать TCNT1, как если бы это была обычная переменная, а любые операции чтения и записи направляются на адрес памяти 0x84 автоматически. Однако он также включает в себя (неявный) reinterpret_cast, который не позволяет использовать адрес этой "переменной" в константном выражении. А так как этот макрос определяется avr-libc, изменение его для удаления приведения на самом деле не вариант (и само переопределение таких макросов работает, но затем требуется определить их для всех различных чипов AVR, дублируя информацию из avr-libc).

Поскольку предложенный Шафиком хакерский расклад больше не работает в gcc 7 и выше, я искал другое решение.

Если присмотреться к заголовочным файлам avr-libc, оказывается, что у них есть два режима:  - Обычно они определяют переменные макросы, как показано выше.  - При использовании внутри ассемблера (или когда он включен с определенным _SFR_ASM_COMPAT), они определяют макросы, которые просто содержат адрес, например:      #define TCNT1 (0x84)

На первый взгляд последнее кажется полезным, поскольку вы могли бы затем установить _SFR_ASM_COMPAT перед включением <avr/io.h> и просто использовать константы intptr_t и использовать адрес напрямую, а не через указатель. Тем не менее, поскольку вы можете включить заголовок avr-libc только один раз (т.е. иметь только TCNT1 в качестве переменной-подобного-макроса или адреса), этот трюк работает только внутри исходного файла, который не содержит никаких других файлов. для этого потребуются переменные типа макросов. На практике это кажется маловероятным (хотя, возможно, у вас могут быть переменные constexpr (class?), Которые объявлены в файле .h и им присвоено значение в файле .cpp, которое больше ничего не содержит?).

В любом случае, я нашел еще один трюк Кристера Уолфридссона, который определяет эти регистры как внешние переменные в заголовочном файле C++, а затем определяет их и находит их в фиксированном месте с помощью файла .S на ассемблере., Затем вы можете просто взять адрес этих глобальных символов, который действителен в выражениях constexpr. Чтобы это работало, этот глобальный символ должен иметь имя, отличное от исходного макроса регистра, чтобы предотвратить конфликт между ними.

Например. в вашем коде C++ вы должны иметь:

extern volatile uint16_t TCNT1_SYMBOL;

struct foo {
  static constexpr volatile uint16_t* ptr = &TCNT1_SYMBOL;
};

А затем вы включаете в проект файл .S, который содержит:

#include <avr/io.h>
.global TCNT1_SYMBOL
TCNT1_SYMBOL = TCNT1

При написании этого я понял, что вышеизложенное не ограничивается случаем AVR-libc, но может также применяться к более общему вопросу, заданному здесь. В этом случае вы можете получить файл C++, который выглядит следующим образом:

extern char MY_PTR_SYMBOL;
struct foo {
  static constexpr const void* ptr = &MY_PTR_SYMBOL;
};

auto main() -> int {
  return 0;
}

И .S файл, который выглядит следующим образом:

.global MY_PTR_SYMBOL
MY_PTR_SYMBOL = 0x1

Вот как это выглядит: https://godbolt.org/z/vAfaS6 (я не мог понять, как заставить проводник компилятора связывать вместе файлы cpp и .S, хотя

Этот подход имеет немного больше стандартного образца, но, похоже, действительно работает надежно в версиях gcc и clang. Обратите внимание, что этот подход похож на аналогичный подход с использованием параметров командной строки компоновщика или сценариев компоновщика для размещения символов по определенному адресу памяти, но этот подход очень непереносим и сложен для интеграции в процессе сборки, в то время как предложенный выше подход является более переносимым и просто вопрос добавления файла .S в сборку.