Forum: PC-Programmierung C++17 initialzer list aus std::array


von nga (Gast)


Lesenswert?

Hallo,

ich würde gerne in einem C++ Projekt ein Grid (2D-Array) aus floats 
beschreiben.

Dafür gibt es eine Klasse
1
#include <cstddef>
2
template<std::size_t rows, std::size_t columns>
3
class grid {
4
    std::array<std::array<float, columns>, rows> elements;
5
public:
6
    constexpr grid(const std::initializer_list<float> list) noexcept;
7
    // ...member functions...
8
};
Aktuell kann ich die Klasse so initialisieren:
1
grid<2,2>{
2
    1, 2,
3
    3, 4
4
};
Dabei ist es allerdings leicht Fehler zu machen, z.B. eine falsche 
Grid-Größe oder zu viele Werte werden angegeben, z.B.:
1
grid<1,3>{
2
    1, 2, 3,
3
    4, 5, 6
4
};
5
// Grid: [[1,2,3]] anstatt den "optisch erwarteten" [[1,2,3],[4,5,6]]
Auch blöd ist, dass man Zeilen und spalten vertauschen kann:
1
grid<4,2>{
2
    1,1,1,1,
3
    2,2,2,2
4
};
5
// Grid: [[1,1],[1,1],[2,2],[2,2]] anstatt [[1,1,1,1],[2,2,2,2]]
Soweit der aktuelle Stand.



Nervig finde ich erstens, dass man die template-Parameter bei dieser 
Version explizit angeben muss (und damit Fehler machen kann) und 
zweitens, dass der Benutzer 1 Array (bzw. eine Initializer list, aber 
die sieht ja aus wie ein Array) angibt, obwohl es ja eine 2D-Struktur 
ist. Ich finde, man sollte im Code sehen, wie groß das Grid ist.

Ich würde nun gerne einen Konstruktor schreiben, der das erlaubt:
1
grid{{1,2},{3,4},{5,6}};
2
// bzw. optisch ansprechender
3
grid{
4
    {1, 2},
5
    {3, 4},
6
    {5, 6}
7
};
8
// Grid: 3x2 [[1,2],[3,4],[5,6]]
9
// Template parameter automatisch erkannt
Schön wäre es außerdem, wenn das Programm nicht kompiliert, wenn die 
inneren Arrays nicht gleich lang sind.

Wichtig ist aber, dass alles constexpr ist, also zum Beispiel mittels 
static_assert() überprüfbar.

Gibt es so ein Konstrukt im aktuellen C++?

Mit freundlichen Grüßen,
nga

Beitrag #5450906 wurde vom Autor gelöscht.
von Wilhelm M. (wimalopaan)


Lesenswert?

Auf die Schnelle sehe ich zwei Varianten:
1
    constexpr auto g = make_grid(1, 2)(3, 4)();
2
    
3
    constexpr auto g = grid{R{1, 2}, R{3, 4}};

Man kann zwar initializer_list verschachteln, allerdings sehe ich gerade 
keine Möglichkeit die Länge einer inilializer_list als constexpr zu 
ermitteln, wenn die initializer_list als Funktionsparameter übergeben 
wird, da sie in diesem Kontext wieder nicht mehr constexpr ist.

Deswegen sehe ich erstmal nur die beiden Möglichkeiten oben. Einmal mit 
einer Factory und op() oder einem Helper-Typ R.

von Wilhelm M. (wimalopaan)


Lesenswert?

Man kann auf den helper make_grid() auch verzichten, wenn man ein DGuide 
einsetzt:
1
    constexpr auto g = grid(1, 2)(3, 4)(5, 6);

Passt das für Dich? Wenn ja, schreibe ich es ausführlich hin ...

: Bearbeitet durch User
von nga (Gast)


Lesenswert?

Hallo Wilhelm M.,

danke für deine Beiträge.
An die Möglichkeit einer Factory habe ich gar nicht gedacht, die Idee 
ist gut.
Der Helper-Typ ist zwar von der Idee auch nicht schlecht und vmtl. 
einfach zu implementieren, aber sagt mir ehrlich gesagt nicht so zu.

Wilhelm M. schrieb:
> Man kann auf den helper make_grid() auch verzichten, wenn man ein DGuide
> einsetzt:
1
     constexpr auto g = grid(1, 2)(3, 4)(5, 6);
> Passt das für Dich?

Diese Möglichkeit finde ich sehr schön, aber ich muss jetzt (mglw. blöd) 
fragen: Was ist ein DGuide? Ich habe diesen Begriff noch nie gehört.

Es wäre nett, wenn du entweder Informationen verlinken könntest oder 
eine Beispiel-Signatur des Konstruktor schreiben würdest (irgendwie 
komme ich gerade nicht drauf, wie man das einfach Implementieren kann).

Grüße,
nga

von Wilhelm M. (wimalopaan)


Lesenswert?

nga schrieb:

> Wilhelm M. schrieb:
>> Man kann auf den helper make_grid() auch verzichten, wenn man ein DGuide
>> einsetzt:
>
1
>      constexpr auto g = grid(1, 2)(3, 4)(5, 6);
2
>
>> Passt das für Dich?
>
> Diese Möglichkeit finde ich sehr schön, aber ich muss jetzt (mglw. blöd)
> fragen: Was ist ein DGuide? Ich habe diesen Begriff noch nie gehört.

Damit ist ein user-defined deduction-guide gemeint. Beispiel kommt 
gleich ...

von Wilhelm M. (wimalopaan)


Lesenswert?

Hier (s.u.) eine Möglichkeit ohne Helper-Funktion. Der "Trick" besteht 
darin, das grid<> ein Functor ist, der hier als factory benutzt wird.
1
#include <cstddef>
2
#include <array>
3
#include <algorithm>
4
#include <iostream>
5
#include <cassert>
6
7
// little bit of TMP (see below: deduction-guide)
8
namespace Meta {
9
    template<typename... T> struct List;
10
    namespace detail {
11
        template<typename L> struct front;
12
        template<typename F, typename... T, template<typename...> typename L>
13
        struct front<L<F, T...>> {
14
            typedef F type;
15
        };
16
    }
17
    template<typename L>
18
    using front = typename detail::front<L>::type;
19
}
20
21
template<typename ItemType, std::size_t rows, std::size_t columns>
22
class grid {
23
    std::array<std::array<ItemType, columns>, rows> elements{};
24
public:
25
    template<typename... VV>
26
    constexpr grid(const VV&... vv) : elements{{ItemType(vv)...}} {
27
        static_assert(sizeof...(VV) == columns, "wrong number of columns");
28
    }
29
30
    template<typename... AA>
31
    constexpr grid(const std::array<ItemType, columns>& a, const AA&... aa) : elements{a, aa...} {
32
        static_assert(sizeof...(AA) == (rows - 1), "wrong number of rows");
33
        static_assert((std::is_same_v<ItemType, typename AA::value_type> && ...));
34
    }
35
    
36
    template<typename... VV>
37
    constexpr auto operator()(VV... rv) {
38
        static_assert(sizeof...(VV) == columns, "wrong number of values to form a complete row");
39
//        static_assert((std::is_same_v<ItemType, VV> && ...), "use always the same type"); 
40
        auto n = [&]<auto... RR>(std::index_sequence<RR...>) { // C++20, refactor to function-template if C++17
41
                std::array<ItemType, columns> nextRow{ItemType(rv)...};
42
                grid<ItemType, rows + 1, columns> n{(*this)[RR]..., nextRow};
43
                return n;
44
        }(std::make_index_sequence<rows>{});
45
        return n;
46
    }
47
    
48
    constexpr auto& operator[](size_t row) const {
49
        assert(row < rows);
50
        return elements[row];
51
    }
52
};
53
54
// user-defined deduction-guide
55
template<typename... RR>
56
grid(RR... rr) -> grid<Meta::front<Meta::List<RR...>>, 1, sizeof...(RR)>;
57
58
int main() {
59
    constexpr auto g1 = grid(1.0, 2.0)(3, 4)(5, 6); // grid<double, 3, 2>
60
    constexpr auto g2 = grid(1, 2)(3, 4)(5, 6); // grid<int, 3, 2>
61
}

Wie schon beschrieben, sind std::initializer_list's hier nur wenig 
geeignet, da sie in einem constexpr-Kontext nur schlecht funktionieren. 
Es gab mal einen Entwurf, die Länge statisch als member-typ zur 
Verfügung zu stellen ...

Mit concepts können man die überladenen ctor'en etwas besser gestalten.

: Bearbeitet durch User
von nga (Gast)


Lesenswert?

Hallo,

danke für das Beispiel!
Ich werde allerdings wohl etwas brauchen, bis ichs komplett verstanden 
habe. Mir sind das irgendwie zu viele "..." :-)

Grüße und nochmals vielen Dank,
nga

von nga (Gast)


Lesenswert?

Hallo,

ich muss mich nochmal als unfähig/unwissend outen... (mach ich nicht 
gerne :-) ).

Betreffend dieses Parts:

Wilhelm M. schrieb:
> template<typename... VV>
>     constexpr auto operator()(VV... rv) {
>         static_assert(sizeof...(VV) == columns, "wrong number of values
> to form a complete row");
> //        static_assert((std::is_same_v<ItemType, VV> && ...), "use
> always the same type");
>         auto n = [&]<auto... RR>(std::index_sequence<RR...>) { // C++20,
> refactor to function-template if C++17
>                 std::array<ItemType, columns> nextRow{ItemType(rv)...};
>                 grid<ItemType, rows + 1, columns> n{(*this)[RR]...,
> nextRow};
>                 return n;
>         }(std::make_index_sequence<rows>{});
>         return n;
>     }
Ich möchte das ganze erst mal mit der aktuellsten verabschiedeten 
Version der Sprache machen, also C++17 (ohne GNU extensions o.Ä.).
Also kann ich das Lambda Template so in der Art (wie du netterweise 
geschrieben hast) nicht verwenden -.-
Ich habe mich mal hingesetzt und versucht das ganze mittels 
template-Funktionen nachzubilden (Compiler Explorer): 
https://godbolt.org/g/ZBvVES

Leider kenne ich mich mit TMP im Prinzip gar nicht aus und ich kann 
weder mit der GCC-Fehlermeldung, noch mit der von clang etwas 
anfangen...

GCC:
1
note:   template argument deduction/substitution failed:
2
note:   mismatched types 'std::integer_sequence<long unsigned int, RR ...>' and 'int'
3
return expand_grid(rv..., std::make_index_sequence<rows>{});
4
                                                              ^
clang:
1
note: in instantiation of function template specialization 'grid<double, 1, 2>::operator()<int, int>' requested here
2
note: candidate template ignored: could not match 'integer_sequence<unsigned long, RR...>' against 'int'
(Genaueres alles im Compiler Explorer zu sehen)

Ich denke, dass ich einen trivialen Fehler

Es wäre furchbar nett, wenn du mir noch einmal auf die Sprünge helfen 
könntest, oder entsprechende Literatur zu aktueller C++ TMP empfehlen 
könntest.

Grüße,
nga

von Wilhelm M. (wimalopaan)


Lesenswert?

Du hast nur die Reihenfolge der Argumente vertauscht:
1
        return expand_grid(std::make_index_sequence<rows>{}, rv... );

Das erste Argument muss ja die std::index_sequence<> sein, danach 
folgende die Werte der neuen Zeile.

von nga (Gast)


Lesenswert?

Manchmal sieht man den Wald vor lauter Bäumen/... nicht :-)

Nochmals vielen Dank für deine Hilfe!

Grüße,
nga

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.