constexpr und Initialisierung eines statischen Const-Void-Zeigers mit Reinterpret-Cast, welcher Compiler ist richtig?

Lesezeit: 11 Minuten

constexpr und Initialisierung eines statischen Const Void Zeigers mit Reinterpret Cast welcher Compiler
101010

Betrachten Sie den folgenden Codeabschnitt:

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

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

Das obige Beispiel lässt sich gut in g++ v4.9 kompilieren (Live-Demo), während es in clang v3.4 nicht kompiliert werden kann (Live-Demo) und erzeugt den folgenden Fehler:

Fehler: Die constexpr-Variable ‘ptr’ muss durch einen konstanten Ausdruck initialisiert werden

Fragen:

  • Welcher der beiden Compiler ist laut Standard richtig?

  • Wie deklariert man einen solchen Ausdruck richtig?

  • Was ist mit euch Leuten mit euren auto main() -> int?

    – Griwes

    25. Juni 2014 um 8:41 Uhr

  • @KerrekSB “Vielleicht ist das OP der Erfinder von XML?” Du hast mich…

    – 101010

    25. Juni 2014 um 19:56 Uhr

  • Mich würde interessieren, was der Anwendungsfall dafür ist?

    – Shafik Yaghmour

    26. Juni 2014 um 4:16 Uhr

  • @ShafikYaghmour Es kann als ein weiteres spezielles “nullptr” verwendet werden. Ein Zeiger kann also drei Zustände haben: gültiger Zeiger, nullptr, besonderer Zustandswert.

    – Bryan Chen

    26. Juni 2014 um 4:26 Uhr

  • Ein weiterer Anwendungsfall wäre die Initialisierung von Zeigerkonstanten, die auf Peripheriegeräte auf Mikrocontrollern zeigen. Diese Adressen werden normalerweise als Integer-Makros in gerätespezifischen Header-Dateien angegeben.

    – Pait

    13. Februar 2015 um 19:24 Uhr

constexpr und Initialisierung eines statischen Const Void Zeigers mit Reinterpret Cast welcher Compiler
Shafik Yaghmur

TL;DR

clang stimmt, das ist bekannt gcc Insekt. Sie können entweder verwenden intptr_t stattdessen und umwandeln, wenn Sie den Wert verwenden müssen oder wenn das nicht funktioniert, dann beides gcc und clang Unterstützung einer kleinen dokumentierten Problemumgehung, die Ihren speziellen Anwendungsfall ermöglichen sollte.

Einzelheiten

Damit clang ist richtig, wenn wir zu dem gehen Entwurf des C++11-Standards Sektion 5.19 Konstante Ausdrücke Absatz 2 sagt:

Ein bedingter Ausdruck ist ein konstanter Kernausdruck, es sei denn, er beinhaltet einen der folgenden als potenziell ausgewerteten Unterausdruck
[…]

und enthält den folgenden Aufzählungspunkt:

— ein reinterpret_cast (5.2.10);

Eine einfache Lösung wäre die Verwendung intptr_t:

static constexpr intptr_t ptr = 0x1;

und dann später werfen, wenn Sie es verwenden müssen:

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

Es mag verlockend sein, es dabei zu belassen, aber diese Geschichte wird interessanter. Das ist bekannt und noch offen gcc Fehler siehe Fehler 49171: [C++0x][constexpr] Konstante Ausdrücke unterstützen reinterpret_cast. Das geht aus der Diskussion hervor gcc Entwickler haben einige klare Anwendungsfälle dafür:

Ich glaube, ich habe eine konforme Verwendung von reinterpret_cast in konstanten Ausdrücken gefunden, die in C ++ 03 verwendet werden können:

//---------------- 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
//----------------

Grundsätzlich demonstriert dieses Programm die Technik, auf der die C++11-Bibliotheksfunktion addressof basiert und somit reinterpret_cast ausschließt
bedingungslos von konstanten Ausdrücken in der Kernsprache würde dieses nützliche Programm ungültig machen und es unmöglich machen, addressof als constexpr-Funktion zu deklarieren.

konnten aber keine Ausnahme für diese Anwendungsfälle erstellen, siehe geschlossene Ausgaben 1384:

Obwohl reinterpret_cast in C++03 in Adresskonstantenausdrücken zulässig war, wurde diese Einschränkung in einigen Compilern implementiert und hat sich nicht als erheblicher Codebruch erwiesen. CWG war der Ansicht, dass die Komplikationen beim Umgang mit Zeigern, deren Typen sich geändert haben (Zeigerarithmetik und Dereferenzierung konnten für solche Zeiger nicht zugelassen werden), den möglichen Nutzen einer Lockerung der aktuellen Beschränkung aufwogen.

ABER offenbar gcc und clang unterstützt eine kleine dokumentierte Erweiterung, die eine konstante Faltung von nicht konstanten Ausdrücken erlaubt __builtin_constant_p (exp) und so werden die folgenden Ausdrücke von beiden akzeptiert gcc und clang:

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

Dokumentation dafür zu finden ist nahezu unmöglich, aber diese llvm Commit ist informativ mit den folgenden Ausschnitten sorgen für interessante Lektüre:

unterstützt den gcc __builtin_constant_p() ? … : … Folding-Hack in C++11

und:

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

und:

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

Wir können eine formellere Erklärung dieser Funktion in der gcc-patches-E-Mail finden: C-Konstantenausdrücke, VLAs usw. behoben was sagt:

Außerdem sind die Regeln für Aufrufe von __builtin_constant_p als bedingte Ausdrucksbedingung in der Implementierung lockerer als im formalen Modell: Die ausgewählte Hälfte des bedingten Ausdrucks wird vollständig gefaltet, ohne Rücksicht darauf, ob es sich formal um einen konstanten Ausdruck handelt, da __builtin_constant_p ein Fully testet gefaltetes Argument selbst.

  • die willkürliche Liste der Ausnahmen von dem, was nicht sein kann constexpr fängt an mich zu ärgern…

    – TemplateRex

    12. Mai 2015 um 10:24 Uhr

  • @TemplateRex, also habe ich mit jemandem im letzten llvm social gechattet und IIUC, der Compiler müsste viele TBAA-Daten verfolgen, um UB in einem constexpr für reinterpret_cast zu erkennen, was sehr teuer wäre. Ich muss nachfassen und sehen, ob ich das richtig gemacht habe.

    – Shafik Yaghmour

    11. Januar 2018 um 17:56 Uhr

  • Ich stimme zu, dass die Verfolgung von UB problematisch ist. OTOH, Dinge wie goto und strukturierte Bindungen sollten in der Lage sein, für constepxr.

    – TemplateRex

    11. Januar 2018 um 20:26 Uhr

  • Es gab einen Constexpr-Goto-Vorschlag, der jedoch zugunsten eines umfassenderen Ansatzes abgelehnt wurde. Siehe Reisebericht von Botond im Abschnitt Abgelehnt.

    – Shafik Yaghmour

    11. Januar 2018 um 20:34 Uhr


  • Wie Kishore oben erwähnte, akzeptiert gcc diesen Trick nicht mehr (6.4 funktioniert noch, 7.1 bricht ab, 8.0 ändert die Fehlermeldung). Clang 9.0 (aktuell neueste Version) akzeptiert es immer noch. Schade.

    – Matthijs Kooijman

    2. Oktober 2019 um 13:54 Uhr

constexpr und Initialisierung eines statischen Const Void Zeigers mit Reinterpret Cast welcher Compiler
Kerrek SB

Klon hat recht. Das Ergebnis eines Reinterpret-Casts ist niemals ein konstanter Ausdruck (vgl. C++11 5.19/2).

Der Zweck konstanter Ausdrücke besteht darin, dass sie als Werte betrachtet werden können und dass Werte gültig sein müssen. Was Sie schreiben, ist nachweislich kein gültiger Zeiger (da es sich nicht um die Adresse eines Objekts handelt oder durch Zeigerarithmetik mit der Adresse eines Objekts zusammenhängt), sodass Sie es nicht als konstanten Ausdruck verwenden dürfen. Wenn Sie nur die Nummer speichern möchten 1speichern Sie es als uintptr_t und führen Sie die Neuinterpretation am Einsatzort durch.


Nebenbei, um den Begriff “gültige Zeiger” ein wenig zu erläutern, betrachten Sie das Folgende constexpr Zeiger:

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

Beide p1 und p2 sind konstante Ausdrücke. Aber ob das Ergebnis der Zeigerarithmetik ein konstanter Ausdruck ist, hängt davon ab, ob es nicht UB ist! Diese Art von Argumentation wäre im Wesentlichen unmöglich, wenn Sie zulassen würden, dass die Werte von reinterpret_casts konstante Ausdrücke sind.

  • Woher wissen Sie, dass das kein gültiger Zeiger ist? Bei mir zeigt es auf Adresse 1, was vollkommen in Ordnung ist. (Mikrocontroller)

    – Deduplizierer

    25. Juni 2014 um 0:04 Uhr

  • @Deduplicator: Wenn Sie wirklich ein Zitat benötigen, nicht nur ein Zitat, dann lautet der referenzierte Abschnitt des Standards “A bedingter Ausdruck ist ein Kern konstanter Ausdruck es sei denn, es handelt sich um eines der folgenden”, wobei die folgende Liste “a reinterpret_cast

    – Mike Seymour

    25. Juni 2014 um 0:19 Uhr

  • @HolyBlackCat: In der Tat, 0 ist eine Nullzeigerkonstante. Aber das hat nichts damit zu tun, beliebige ganzzahlige Werte als Zeiger neu zu interpretieren.

    – Mike Seymour

    25. Juni 2014 um 0:21 Uhr

  • @Deduplikator: konstante Ausdrücke sind eine Teilmenge von Kernkonstantenausdrückewie im folgenden Absatz 5.19/3 beschrieben.

    – Mike Seymour

    25. Juni 2014 um 0:33 Uhr

  • Der erste Absatz in Ihrer Antwort ist zweifellos gültig. Der zweite Absatz, nicht so sehr. Deduplicator ist absolut korrekt. Es ist ein gültiger Zeiger auf Systemen, in denen 0x1 ist die Adresse eines Bytes im Speicher, obwohl es sich nicht um einen konstanten Ausdruck handelt.

    – Ben Voigt

    25. Juni 2014 um 1:18 Uhr

Ich bin auch auf dieses Problem gestoßen, als ich für AVR-Mikrocontroller programmiert habe. Avr-libc hat Header-Dateien (enthalten durch <avr/io.h> die das Registerlayout für jeden Mikrocontroller per Definition zur Verfügung stellen Makros wie z:

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

Dies ermöglicht die Verwendung TCNT1 als wäre es eine normale Variable und alle Lese- und Schreibvorgänge werden automatisch an die Speicheradresse 0x84 geleitet. Es enthält aber auch eine (implizite) reinterpret_cast, was verhindert, dass die Adresse dieser “Variablen” in einem konstanten Ausdruck verwendet wird. Und da dieses Makro von avr-libc definiert wird, ist es nicht wirklich eine Option, es zu ändern, um den Cast zu entfernen (und solche Makros selbst neu zu definieren, funktioniert, erfordert dann aber, sie für alle verschiedenen AVR-Chips zu definieren und die Informationen von avr-libc zu duplizieren) .

Da der von Shafik hier vorgeschlagene Folding-Hack in gcc 7 und höher nicht mehr zu funktionieren scheint, habe ich nach einer anderen Lösung gesucht.

Wenn man sich die Header-Dateien von avr-libc genauer ansieht, ist es stellt sich heraus, dass sie zwei Modi haben: – Normalerweise definieren sie variablenähnliche Makros wie oben gezeigt. – Bei Verwendung im Assembler (oder im Lieferumfang von _SFR_ASM_COMPAT definiert), sie definieren Makros, die nur die Adresse enthalten, zB: #define TCNT1 (0x84)

Letzteres erscheint auf den ersten Blick sinnvoll, da man es dann einstellen könnte _SFR_ASM_COMPAT vor einschließen <avr/io.h> und einfach verwenden intptr_t Konstanten und verwenden Sie die Adresse direkt statt über einen Zeiger. Da Sie den avr-libc-Header jedoch nur einmal einbinden können (iow, only have TCNT1 entweder als variablenähnliches Makro oder als Adresse), funktioniert dieser Trick nur innerhalb einer Quelldatei, die keine anderen Dateien enthält, die die variablenähnlichen Makros benötigen würden. In der Praxis erscheint dies unwahrscheinlich (obwohl Sie vielleicht constexpr-Variablen (Klasse?) haben könnten, die in einer .h-Datei deklariert und einem Wert in einer .cpp-Datei zugewiesen werden, die nichts anderes enthält?).

Jedenfalls fand ich ein weiterer Trick von Krister Walfridsson, die diese Register als externe Variablen in einer C++-Headerdatei definiert und sie dann definiert und mithilfe einer Assembler-S-Datei an einem festen Ort lokalisiert. Dann können Sie einfach die Adresse dieser globalen Symbole nehmen, die in einem constexpr-Ausdruck gültig ist. Damit dies funktioniert, muss dieses globale Symbol einen anderen Namen haben als das ursprüngliche Registermakro, um einen Konflikt zwischen beiden zu vermeiden.

In Ihrem C++-Code hätten Sie z. B.:

extern volatile uint16_t TCNT1_SYMBOL;

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

Und dann fügen Sie eine .S-Datei in Ihr Projekt ein, die Folgendes enthält:

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

Während ich dies schrieb, wurde mir klar, dass das Obige nicht auf den AVR-libc-Fall beschränkt ist, sondern auch auf die hier gestellte allgemeinere Frage angewendet werden kann. In diesem Fall könnten Sie eine C++-Datei erhalten, die wie folgt aussieht:

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

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

Und eine .S-Datei, die so aussieht:

.global MY_PTR_SYMBOL
MY_PTR_SYMBOL = 0x1

So sieht das aus: https://godbolt.org/z/vAfaS6 (Ich konnte jedoch nicht herausfinden, wie ich den Compiler-Explorer dazu bringen könnte, sowohl die cpp- als auch die .S-Datei miteinander zu verknüpfen

Dieser Ansatz hat einiges mehr Boilerplate, scheint aber zuverlässig über gcc- und Clang-Versionen hinweg zu funktionieren. Beachten Sie, dass dieser Ansatz wie ein ähnlicher Ansatz aussieht, der Linker-Befehlszeilenoptionen oder Linker-Skripts verwendet, um Symbole an einer bestimmten Speicheradresse zu platzieren, aber dieser Ansatz ist höchst nicht portabel und schwierig in einen Build-Prozess zu integrieren, während der oben vorgeschlagene Ansatz portabler ist und nur eine Frage des Hinzufügens einer .S-Datei zum Build.

Dies ist keine universelle Antwort, aber es funktioniert mit dem Sonderfall einer Struktur mit speziellen Funktionsregistern eines MCU-Peripheriegeräts an fester Adresse. Eine Vereinigung könnte verwendet werden, um eine Ganzzahl in einen Zeiger umzuwandeln. Es ist immer noch ein undefiniertes Verhalten, aber dieses Cast-by-Union ist in einem eingebetteten Bereich weit verbreitet. Und es funktioniert perfekt in GCC (getestet bis 9.3.1).

struct PeripheralRegs
{
    volatile uint32_t REG_A;
    volatile uint32_t REG_B;
};

template<class Base, uintptr_t Addr>
struct SFR
{
    union
    {
        uintptr_t addr;
        Base* regs;
    };
    constexpr SFR() :
        addr(Addr) {}
    Base* operator->() const
    {
        return regs;
    }
    void wait_for_something() const
    {
        while (!regs->REG_B);
    }
};

constexpr SFR<PeripheralRegs, 0x10000000> peripheral;

uint32_t fn()
{
    peripheral.wait_for_something();
    return peripheral->REG_A;
}

924880cookie-checkconstexpr und Initialisierung eines statischen Const-Void-Zeigers mit Reinterpret-Cast, welcher Compiler ist richtig?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy