web-dev-qa-db-de.com

C++ "vergisst", dass die Variable constexpr ist, wenn sie als Funktionsargument verwendet wird

Ich habe den folgenden Code, in dem ich irritiert darüber bin, dass der Compiler nicht erkennen kann, dass die als Argument an eine Funktion übergebene Variable constexpr ist. Daher muss ich die Funktion arity 0 anstelle der Funktion 1 Argument verwenden.

Ich weiß, dass dies kein Compiler-Fehler ist, aber ich frage mich, ob es Redewendungen gibt, die es ermöglichen, dieses Problem zu umgehen.

#include <array>
#include <iostream>

static constexpr std::array<int, 5> arr{11, 22, 33, 44, 55};

template <typename C, typename P, typename Y>
static constexpr void copy_if(const C& rng, P p, Y yi3ld) {
    for (const auto& elem: rng) {
        if (p(elem)){
            yi3ld(elem);
        }
    }
}

// template<std::size_t N>
static constexpr auto get_evens(/* const std::array<int, N>& arr */) {
    constexpr auto is_even = [](const int i) constexpr {return i % 2 == 0;};
    constexpr int cnt = [/* &arr, */&is_even]() constexpr {
        int cnt = 0;
        auto increment = [&cnt] (const auto&){cnt++;};
        copy_if(arr, is_even, increment);
        return cnt;
    }();
    std::array<int, cnt> result{};
    int idx = 0;
    copy_if(arr, is_even, [&result, &idx](const auto& val){ result[idx++] = val;});
    return result;
}

int main() {
    // constexpr std::array<int, 5> arr{11, 22, 33, 44, 55};
    for (const int i:get_evens(/* arr */)) {
        std::cout << i << " " << std::endl;
    }
}

Wenn nicht klar ist, was ich will: Ich möchte die Signatur von get_evens so ändern, dass sie als Vorlage für die Array-Größe N dient und 1 Argument vom Typ const std::array<int, N>& akzeptiert.

Die Fehlermeldung, wenn ich arr in ein Funktionsargument ändere, ist nicht hilfreich:

prog.cc:25:21: note: der initializer von 'cnt' ist kein konstanter ausdruck prog.cc:19:19: note: hier deklariert constexpr int cnt = [&arr, &is_even]()constexpr {

6
NoSenseEtAl
#include <array>
#include <iostream>

static constexpr std::array<int, 5> arr{11, 22, 33, 44, 55};

template <typename C, typename P, typename T>
static constexpr void invoke_if(const C& rng, P p, T target) {
    for (const auto& elem: rng) {
        if (p(elem)){
            target(elem);
        }
    }
}

constexpr bool is_even(int i) {
    return i % 2 == 0;
}

template<std::size_t N>
constexpr std::size_t count_evens(const std::array<int, N>& arr)
{
    std::size_t cnt = 0;
    invoke_if(arr, is_even, [&cnt](auto&&){++cnt;});
    return cnt;
}

template<std::size_t cnt, std::size_t N>
static constexpr auto get_evens(const std::array<int, N>& arr) {
    std::array<int, cnt> result{};
    int idx = 0;
    invoke_if(arr, is_even, [&result, &idx](const auto& val){ result[idx++] = val;});
    return result;
}

int main() {
    // constexpr std::array<int, 5> arr{11, 22, 33, 44, 55};
    for (const int i:get_evens<count_evens(arr)>(arr)) {
        std::cout << i << " " << std::endl;
    }
}

dies funktioniert in g ++ , aber in Klänge bekommen wir ein Problem, weil die begin auf einer array mit mindestens einer Bibliothek nicht richtig constexpr ist. Oder vielleicht verstößt g ++ gegen den Standard und clang nicht.

Ein Funktionsargument ist niemals ein konstanter Ausdruck, auch wenn eine Funktion im Kontext constexpr verwendet wird:

constexpr int foo(int i)
{
    // i is not a constexpr
    return i + 1;
}

constexpr auto i = 1;
constexpr auto j = foo(i);    

Verwenden Sie einen Vorlagenparameter, um ein constexpr-Argument zu imitieren:

template<int i>
constexpr int foo()
{
    // i is constexpr
    return i + 1;
}

constexpr auto i = 1;
constexpr auto j = foo<i>();

Eine mögliche Lösung besteht darin, std::integer_sequence zu verwenden, um Ganzzahlen in einen Typ zu kodieren:

#include <array>
#include <iostream>
#include <type_traits>

template<typename P, typename Y, int... elements>
constexpr void copy_if_impl(P p, Y yi3ld, std::integer_sequence<int, elements...>) {
    ((p(elements) && (yi3ld(elements), true)), ...);
}

template<typename arr_t, typename P, typename Y>
constexpr void copy_if(P p, Y yi3ld) {
    copy_if_impl(p, yi3ld, arr_t{});
}

template<typename arr_t>
constexpr auto get_evens(){
    constexpr auto is_even = [](const int i) constexpr { return i % 2 == 0; };
    constexpr int cnt = [&is_even]() constexpr {
        int cnt = 0;
        auto increment = [&cnt](const auto&) { cnt++; };
        copy_if<arr_t>(is_even, increment);
        return cnt;
    }();

    std::array<int, cnt> result{};
    int idx = 0;
    copy_if<arr_t>(is_even, [&result, &idx](const auto& val) {
        result[idx++] = val; });
    return result;
}

int main()
{
    using arr = std::integer_sequence<int, 11, 22, 33, 44, 55>;
    for (const int i : get_evens<arr>()) {
        std::cout << i << " " << std::endl;
    }
}

Ergänzung von Constantinos Glynos vorgeschlagen.

Aus Effective Modern C++ Buch von Scott Meyers , Punkt 15, S.98:

  • constexpr-Funktionen können in Kontexten verwendet werden, in denen Konstanten für die Kompilierungszeit erforderlich sind. Wenn die Werte der Argumente, die Sie in einem solchen Kontext an eine constexpr-Funktion übergeben, während der Kompilierung bekannt sind, wird das Ergebnis während der Kompilierung berechnet. Wenn während des Kompilierens einer der Argumentwerte nicht bekannt ist, wird Ihr Code abgelehnt.
  • Wenn eine constexpr -Funktion mit einem oder mehreren Werten aufgerufen wird, die während der Kompilierung nicht bekannt sind, verhält sie sich wie eine normale Funktion und berechnet ihr Ergebnis zur Laufzeit. Dies bedeutet, dass Sie nicht zwei Funktionen benötigen, um denselben Vorgang auszuführen, eine für Konstanten zur Kompilierungszeit und eine für alle anderen Werte. Die Funktion constexpr macht alles.
9
Evg

Die andere Antwort hat eine korrekte Problemumgehung, aber ich denke, die Argumentation hat nichts mit Parametern zu tun, sondern mit der Lambda-Erfassung hier:

constexpr int cnt = [/* &arr, */&is_even]() 

In der Tat können wir die verschiedenen Szenarien mit diesem Code testen:

#include <array> 
#include <iostream>

template <size_t N>
constexpr int foo(const std::array<int, N>& arr) {
    return [&arr] () { return arr.size(); }();
}

template <size_t N>
constexpr int bar(const std::array<int, N>& arr) {
    int res{};
    for (auto i : arr) {
        res++;
    }
    return res;
}

template <size_t N>
constexpr int baz(const std::array<int, N>& arr)     {
    constexpr int test = [&arr] () constexpr {
        return bar(arr);
    }();
    return test;
}

int main() {
    constexpr std::array<int, 5> arr{11, 22, 33, 44, 55};
    constexpr std::array<int, foo(arr)> test{};
    constexpr std::array<int, bar(arr)> test2{};
    constexpr std::array<int, baz(arr)> test3{};
}   

Beachten Sie, dass die Zeile, in der test3 initialisiert wird, nicht kompiliert werden kann. Dies kompiliert jedoch ganz gut:

template <size_t N>
constexpr int baz(const std::array<int, N>& arr) {
    return bar(arr);
}

Also, was ist das Problem hier? Nun, Lambdas sind wirklich nur verherrlichte Funktoren, und im Inneren sieht es ungefähr so ​​aus:

struct constexpr_functor {
    const std::array<int, 5>& arr;
    constexpr constexpr_functor(const std::array<int, 5>& test)
        : arr(test) { }
    constexpr int operator()() const {
        return bar(arr);
    }
};
// ...
constexpr constexpr_functor t{arr};
constexpr std::array<int, t()> test3{};

Beachten Sie jetzt, dass wir eine Fehlermeldung erhalten, die das eigentliche Problem zeigt:

test.cpp:36:33: note: reference to 'arr' is not a constant expression
test.cpp:33:34: note: declared here
    constexpr std::array<int, 5> arr{11, 22, 33, 44, 55};

Die andere Antwort zitiert Scotts Meyers Buch, interpretiert die Zitate jedoch falsch. Das Buch enthält einige Beispiele für Parameter, die in Constexpr-Situationen verwendet werden. In den Anführungszeichen wird jedoch lediglich angegeben, dass die Funktion beim Kompilieren ausgeführt werden kann, wenn Sie einen Nicht-Constexpr-Parameter übergeben.

2
user10506568

Entsprechend dem Vorschlag von Evg können Sie die Zahlen direkt in get_evens() verwenden, indem Sie die Zahlen als Vorlagenparameter eines std::integer_sequence übergeben, die Ganzzahlsequenz jedoch als Argument der Funktion get_evens() übergeben und nicht als Vorlagenparameter.

Ich meine ... du kannst die get_evens() wie folgt vereinfachen (EDIT: weiter vereinfacht nach einem Vorschlag von Evg (Danke!))

template <typename T, T ... Ts>
constexpr auto get_evens (std::integer_sequence<T, Ts...> const &)
 {
   std::array<T, (std::size_t(!(Ts & T{1})) + ...)> result{};

   std::size_t idx = 0;

   ((void)(Ts & 1 || (result[idx++] = Ts, true)), ...);

   return result;
 } 

und Sie können es auf diese Weise verwenden

int main()
 {
   using arr = std::integer_sequence<int, 11, 22, 33, 44, 55>;

   for ( const int i : get_evens(arr{}) )
      std::cout << i << " " << std::endl;
 }
2
max66
template<auto t0, auto...ts>
struct ct_array:
  std::array<decltype(t0) const, 1+sizeof...(ts)>,
  std::integer_sequence<decltype(t0), t0, ts...>
{
  ct_array():std::array<decltype(t0) const, 1+sizeof...(ts)>{{t0, ts...}} {};
};

template<class target, auto X>
struct Push;
template<auto X>
struct Push<void, X>{using type=ct_array<X>;};
template<auto...elems, auto X>
struct Push<ct_array<elems...>, X>{using type=ct_array<elems...,X>;};
template<class target, auto X>
using Push_t= typename Push<target, X>::type;

template<class target>
struct pop;
template<auto x>
struct pop<ct_array<x>>{using type=void;};
template<auto x0, auto...xs>
struct pop<ct_array<x0, xs...>>{using type=ct_array<xs...>;};
template<class target>
using pop_t=typename pop<target>::type;

template<class lhs, class rhs, class F, class=void>
struct transcribe;
template<class lhs, class rhs, class F>
using transcribe_t = typename transcribe<lhs, rhs, F>::type;

template<auto l0, auto...lhs, class rhs, class F>
struct transcribe<ct_array<l0, lhs...>, rhs, F,
  std::enable_if_t<F{}(l0) && sizeof...(lhs)>
>:
  transcribe<pop_t<ct_array<l0, lhs...>>, Push_t<rhs, l0>, F>
{};
template<auto l0, auto...lhs, class rhs, class F>
struct transcribe<ct_array<l0, lhs...>, rhs, F,
  std::enable_if_t<!F{}(l0) && sizeof...(lhs)>
>:
  transcribe<pop_t<ct_array<l0, lhs...>>, rhs, F>
{};
template<auto lhs, class rhs, class F>
struct transcribe<ct_array<lhs>, rhs, F, void>
{
  using type=std::conditional_t< F{}(lhs), Push_t<rhs, lhs>, rhs >;
};
template<class lhs, class F>
using filter_t = transcribe_t<lhs, void, F>;

// C++20
//auto is_even = [](auto i)->bool{ return !(i%2); };
struct is_even_t {
  template<class T>
  constexpr bool operator()(T i)const{ return !(i%2); }
};
constexpr is_even_t is_even{};

template<auto...is>
static constexpr auto get_evens(ct_array<is...>) {
  return filter_t< ct_array<is...>, decltype(is_even) >{};
}

Live-Beispiel .

Testcode:

auto arr = ct_array<11, 22, 33, 44, 55>{};
for (const int i : get_evens(arr)) {
    std::cout << i << " " << std::endl;
}