Wie man das Problem "Lesen der Nicht-Constexpr-Variablen 'a' ist in einem konstanten Ausdruck nicht erlaubt" mit boost.hana löst

8

Ich verwende c ++ 17 mit Boost.hana, um einige Metaprogrammierprogramme zu schreiben. Ein Problem, das mir aufgefallen ist, ist, welche Art von Ausdruck in einem constexpr-Kontext wie static_assert verwendet werden kann. Hier ist ein Beispiel:

#include <boost/hana.hpp>

using namespace boost::hana::literals;

template <typename T>
class X {
public:
    T data;

    constexpr explicit X(T x) : data(x) {}

    constexpr T getData() {
        return data;
    }
};


int main() {
    {   // test1
        auto x1 = X(1_c);
        static_assert(x1.data == 1_c);
        static_assert(x1.getData() == 1_c);
    }
    {   //test2.1
        auto x2 = X(boost::hana::make_tuple(1_c, 2_c));
        static_assert(x2.data[0_c] == 1_c);

        // static_assert(x2.getData()[0_c] == 1_c); // read of non-constexpr variable 'x2' is not allowed in a constant expression
    }
    {   //test2.2
        auto x2 = X(boost::hana::make_tuple(1_c, 2_c));
        auto data = x2.getData();
        static_assert(data[0_c] == 1_c);
    }
}

Zuerst schreibe ich eine Klasse X mit einem Felddaten und einem Accessor getData () . In der main () ‚s test1 Teil, x1.data und x1.getData () verhalten sich gleich wie ich erwartet hatte. Aber im Test2- Teil static_assert(x2.data[0_c] == 1_c)verhält sich das Ändern des Arguments in ein Boost :: Hanas-Tupel immer noch gut, static_assert(x2.getData()[0_c] == 1_c)schlägt jedoch bei der Kompilierung fehl, wobei der Fehler ' Lesen der Nicht-Constexpr-Variablen' x2 'in einem konstanten Ausdruck nicht zulässig ist '. Was weired ist , wenn ich aufgeteilt x2.getData()[0_c]in auto data = x2.getData();und static_assert(data[0_c] == 1_c);kompiliert es wieder gut. Ich würde erwarten, dass sie sich gleich verhalten. Kann jemand helfen zu erklären, warum x2.getData()[0_c]in static_assert in diesem Beispiel nicht verwendet werden kann?

Zum Reproduzieren: clang ++ 8.0 -I / path / to / hana-1.5.0 / include -std = c ++ 17 Test.cpp

Lange
quelle
Interessanterweise kompiliert GCC es erfolgreich . Ich habe den Code auf diesen kürzeren reduziert .
xskxzr
constexprfehlt auf x2und data, constauf getData. godbolt.org/z/ZNL2BK
Maxim Egorushkin
@xskxzr Leider habe ich nur Clang für mein Ziel zu verwenden.
Langer
Siehe auch
xskxzr

Antworten:

5

Das Problem ist, dass boost::hana::tuplees keinen Kopierkonstruktor gibt.

Es hat einen Konstruktor , der wie ein Kopierkonstruktor aussieht :

template <typename ...dummy, typename = typename std::enable_if<
    detail::fast_and<BOOST_HANA_TT_IS_CONSTRUCTIBLE(Xn, Xn const&, dummy...)...>::value
>::type>
constexpr tuple(tuple const& other)
    : tuple(detail::from_index_sequence_t{},
            std::make_index_sequence<sizeof...(Xn)>{},
            other.storage_)
{ }

Da es sich jedoch um eine Vorlage handelt, handelt es sich nicht um einen Kopierkonstruktor .

Da boost::hana::tuplekeine Kopie Konstruktor wird eine implizit deklarierte und als ausgefallen definiert (es nicht unterdrückt wird , da boost::hana::tuplenicht hat keine Kopie oder verschieben Bauer oder Zuweisungsoperatoren, weil, Sie ahnen es, sie können nicht Vorlagen sein).

Hier sehen wir Implementierungsdivergenzen , die sich im Verhalten des folgenden Programms zeigen:

struct A {
    struct B {} b;
    constexpr A() {};
    // constexpr A(A const& a) : b{a.b} {}    // #1
};
int main() {
    auto a = A{};
    constexpr int i = (A{a}, 0);
}

gcc akzeptiert, während Clang und MSVC ablehnen, aber akzeptieren, wenn die Zeile nicht kommentiert #1ist. Das heißt, die Compiler sind sich nicht einig darüber, ob der implizit definierte Kopierkonstruktor einer nicht (direkt) leeren Klasse im Kontext einer konstanten Auswertung verwendet werden darf.

Gemäß der Definition des implizit definierten Kopierkonstruktors gibt es keine Möglichkeit, dass sich # 1 von der von constexpr A(A const&) = default;gcc unterscheidet . Beachten Sie auch, dass, wenn wir B einen benutzerdefinierten constexpr-Kopierkonstruktor geben, Clang und MSVC erneut akzeptieren, das Problem anscheinend darin besteht, dass diese Compiler die constexpr-Kopierkonstruktierbarkeit rekursiv leerer implizit kopierbarer Klassen nicht verfolgen können. Fehler für MSVC und Clang behoben ( behoben für Clang 11).

Beachten Sie, dass die Verwendung von operator[]ein roter Hering ist; Die Frage ist, ob die Compiler den Aufruf getData()(welche Kopierkonstrukte T) in einem Kontext mit konstanter Auswertung zulassen, wie z static_assert.

Offensichtlich wäre die ideale Lösung für Boost.Hana, so zu korrigieren boost::hana::tuple, dass es tatsächliche Kopier- / Verschiebungskonstruktoren und Kopier- / Verschiebungszuweisungsoperatoren hat. (Dies würde Ihren Anwendungsfall beheben, da der Code vom Benutzer bereitgestellte Kopierkonstruktoren aufruft, die im Kontext einer konstanten Auswertung zulässig sind.) Um dieses Problem getData()zu umgehen , können Sie Hacking in Betracht ziehen , um den Fall eines nicht zustandsbehafteten Falls zu erkennen T:

constexpr T getData() {
    if (data == T{})
        return T{};
    else
        return data;
}
ecatmur
quelle
Ich verstehe nicht, warum der Standardkopierkonstruktor nicht funktioniert ... Können Sie versuchen, es mir noch einmal zu erklären?
Antoine Morrier
@AntoineMorrier Ich glaube, das ist ein Fehler im Klirren. Es scheint schwierig zu sein zu erkennen, dass rekursiv leere Klassen im Kontextkontext kopiert werden können.
ecatmur
Vielen Dank. Sehr hilfreiche Antwort! Können Sie also weiter erklären, warum der test2.2Teil von Clang akzeptiert wurde? (Ich habe die ursprüngliche Frage bearbeitet und test2 in test2.1 und test2.2 aufgeteilt.) Ich hatte erwartet, dass sie sich gleich verhalten.
Langer
@Long das Bit, mit dem Clang unzufrieden ist, ist die Kopierkonstruktion hana::tuple, die bei der Rückkehr von auftritt getData. In Test2.2 erfolgt die Kopie außerhalb des Kontexts der konstanten Auswertung, sodass Clang damit einverstanden ist.
ecatmur
ähm .. es ist ein bisschen schwierig, zumindest für mich, es zu verstehen .. getData()ist hier nicht erlaubt, aber raus und ein Temperament einzuführen, das dann akzeptiert wird ..
Long
1

Das Problem liegt darin, dass Sie versuchen, einen Laufzeitwert abzurufen und bei der Kompilierung zu testen.

Was Sie tun können, ist, den Ausdruck zur Kompilierungszeit durch a zu zwingen decltypeund es wird wie ein Zauber funktionieren :).

static_assert(decltype(x2.getData()[0_c]){} == 1_c);

#include <boost/hana.hpp>

using namespace boost::hana::literals;

template <typename T>
class X {
public:
    T data;

   constexpr explicit X(T x) : data(x) {}

   constexpr T getData() {
        return data;
    }
};


int main() {
    {   // test1
        auto x1 = X(1_c);
        static_assert(x1.data == 1_c);
        static_assert(x1.getData() == 1_c);
    }
    {   //test2
        auto x2 = X(boost::hana::make_tuple(1_c, 2_c));
        static_assert(x2.data[0_c] == 1_c);

         static_assert(decltype(x2.getData()[0_c]){} == 1_c);

        auto data = x2.getData();
        static_assert(data[0_c] == 1_c);
    }
}

Jetzt wird der Ausdruck zur Kompilierungszeit ausgewertet, sodass der Typ zur Kompilierungszeit bekannt ist. Da er auch zur Rechenzeit konstruierbar ist, kann er in einem static_assert verwendet werden

Antoine Morrier
quelle
Danke für die Antwort. Ich kann verstehen, dass die Verwendung static_assert(decltype(x2.getData()[0_c]){} == 1_c)gut funktionieren kann, aber ich möchte das trotzdem speichern, decltypeda dies viel sparen würde. Sie sagen, Sie sagen x2.getData(), dass ein Laufzeitwert abgerufen wird, damit er nicht in einem static_assert-Ausdruck angezeigt wird. Dann verstehe ich nicht, warum der x1.getData()Teil in Test1 und die Daten in data[0_c]Teil Test2 gut funktionieren können. Was sind ihre Unterschiede?
Langer
Es wird auf keinen Fall auf einen Laufzeitwert zugegriffen, aber vielleicht erlaubt der Standard dem Compiler nicht, dies zu überprüfen.
Jason Rice
0

Zuallererst fehlt Ihnen das const-Qualifikationsmerkmal in der getData()Methode, also sollte es sein:

constexpr T getData() const

Zumindest aus Standardsicht wird keine Variable als constexpr befördert, wenn sie nicht als constexpr markiert ist.

Beachten Sie, dass dies nicht erforderlich x1ist X, da es sich 1_cum einen Typ handelt, der auf hana :: Integral_constant spezialisiert ist, da das Ergebnis ein Typ ohne benutzerdefinierten Kopierkonstruktor ist, der intern keine Daten enthält, sodass eine Kopieroperation getData()in tatsächlich ein No-Op ist , also Ausdruck: static_assert(x1.getData() == 1_c); ist in Ordnung, da keine tatsächliche Kopie erstellt wird (und auch kein Zugriff auf einen nicht konstanten thisZeiger von x1erforderlich ist).

Dies ist sehr unterschiedlich für Ihren Container, hana::tupleder eine sachliche Kopienkonstruktion hana::tupleaus Daten enthältx2.data Feld enthält. Dies erfordert einen sachlichen Zugriff auf Ihren thisZeiger - was im Fall von nicht erforderlich x1war und auch keine constexpr-Variable war.

Dies bedeutet , dass Sie äußern Ihre Absicht falsch mit beiden x1und x2, und es ist notwendig, zumindest für x2diese Variablen als constexpr zu markieren. Beachten Sie auch, dass die Verwendung von leerem Tupel, einer grundsätzlich leeren Spezialisierung (keine benutzerdefinierten Kopierkonstruktoren) im Allgemeinen hana::tuple, nahtlos funktioniert (Abschnitt test3):

#include <boost/hana.hpp>

using namespace boost::hana::literals;

template <typename T>
class X {
public:
    T data;

    constexpr explicit X(T x) : data(x) {}

    constexpr T getData() const {
        return data;
    }
};

template<typename V>
constexpr auto make_X(V value)
{
    return value;
}

int main() {
    {   // test1
        auto x1 = X(1_c);
        static_assert(x1.data == 1_c);
        static_assert(x1.getData() == 1_c);
    }
    {   //test2
        constexpr auto x2 = X(boost::hana::make_tuple(1_c, 2_c));
        static_assert(x2.data[0_c] == 1_c);

        static_assert(x2.getData()[0_c] == 1_c); // read of non-constexpr variable 'x2' is not allowed in a constant expression

        auto data = x2.getData();
        static_assert(data[0_c] == 1_c);
    }
    {   //test3
        auto x3 = X(boost::hana::make_tuple());
        static_assert(x3.data == boost::hana::make_tuple());

        static_assert(x3.getData() == boost::hana::make_tuple());
    }
}
Michał Łoś
quelle
Ich habe noch nicht alle Antworten gelesen, aber eine constexpr-Methode kann normalerweise nicht konstant sein.
Antoine Morrier
Ich bin nicht der Meinung, dass dies x1ein leerer Typ ist. Jede Instanz von Xhat ein Datenelement. Das hana::tupleEnthalten leerer Typen ist selbst leer, da die Optimierung der leeren Basis verwendet wird. Möglicherweise geben Sie dem Kopierkonstruktor die Schuld, weil Clang oder libc ++ möglicherweise etwas Wackeliges tun std::integral_constant.
Jason Rice
Und gibt es eine Möglichkeit, dass ich keinen constexpr für die x2-Deklaration hinzufügen muss? Ich möchte, dass X sowohl mit einem konstanten Wert als auch mit einem Laufzeitwert initialisiert werden kann. Wie zum Beispiel: `` `int g = 1; int main () {{/ * test3 * / auto x3 = X (g); }} `` `Ich hoffe es kann auch perfekt funktionieren. Das Hinzufügen eines constexpr zu x3 wird jedoch nicht kompiliert, mit dem Fehler:constexpr variable 'x3' must be initialized by a constant expression
Langer
@AntoineMorrier: Ja, aber dies ist in Ordnung, solange Sie keinen const thisZeiger verwenden und ihn leider x2im Fall static_assert verwenden. (im Fall von x1 - es ist eine weitere Diskussion :)).
Michał Łoś
@ JasonRice: Ja, das stimmt, ich werde die Antwort optimieren, da ich nicht präzise war: Beide haben natürlich keine nicht statischen Felder. Beachten Sie jedoch, und dies ist das, was meiner Antwort fehlt, dass hana::integral_constantder vom Compiler definierte Standardkonstruktor hana::tuplezwar einen benutzerdefinierten , aber einen benutzerdefinierten Konstruktor hat. Da es eine Spezialisierung für leere Tupel gibt, die keinen Konstruktor hat, funktioniert der gleiche Code für leere Tupel: godbolt.org/z/ZeEVQN
Michał Łoś