Wie kann ich meinen benutzerdefinierten Typ so gestalten, dass er mit "bereichsbasiert für Schleifen" funktioniert?


252

Wie viele Leute in diesen Tagen habe ich die verschiedenen Funktionen von C ++ 11 ausprobiert. Einer meiner Favoriten ist der "Range-Based for Loops".

Ich verstehe das:

for(Type& v : a) { ... }

Ist äquivalent zu:

for(auto iv = begin(a); iv != end(a); ++iv)
{
  Type& v = *iv;
  ...
}

Und das begin()kehrt einfach a.begin()für Standardcontainer zurück.

Aber was ist, wenn ich meinen benutzerdefinierten Typ "bereichsbasiert für Schleife" aktivieren möchte ?

Soll ich mich nur spezialisieren begin()und end()?

Wenn mein benutzerdefinierter Typ zum Namespace gehört xml, sollte ich xml::begin()oder definieren std::begin()?

Kurz gesagt, was sind die Richtlinien, um das zu tun?


Es ist möglich, entweder ein Mitglied begin/endoder einen Freund zu definieren, statisch oder frei begin/end. Seien
alfC

Könnte jemand bitte eine Antwort mit dem Beispiel eines Float-Wertebereichs posten, der KEIN Container ist : for( auto x : range<float>(0,TWO_PI, 0.1F) ) { ... }. Ich bin gespannt, wie Sie die Tatsache umgehen, dass "Operator! = ()" Schwer zu definieren ist. Und was ist *__beginin diesem Fall mit der Dereferenzierung ( )? Ich denke, es wäre ein großartiger Beitrag, wenn uns jemand zeigen würde, wie das gemacht wird!
BitTickler

Antworten:


183

Der Standard wurde geändert, seit die Frage (und die meisten Antworten) in der Lösung dieses Fehlerberichts veröffentlicht wurden .

Es gibt zwei Möglichkeiten, wie Sie eine for(:)Schleife für Ihren Typ verwenden Xkönnen:

  • Erstellen Sie Mitglied X::begin()und X::end()dass die Rückkehr etwas , das wie ein Iterator wirkt

  • Erstellen Sie eine freie Funktion begin(X&)und end(X&)dass die Rückkehr etwas , das wie ein Iterator fungiert, im gleichen Namensraum wie Ihre Art X

Und ähnlich für constVariationen. Dies funktioniert sowohl bei Compilern, die die Änderungen des Fehlerberichts implementieren, als auch bei Compilern, die dies nicht tun.

Die zurückgegebenen Objekte müssen keine Iteratoren sein. Die for(:)Schleife wird im Gegensatz zu den meisten Teilen des C ++ - Standards so spezifiziert , dass sie wie folgt erweitert wird :

for( range_declaration : range_expression )

wird:

{
  auto && __range = range_expression ;
  for (auto __begin = begin_expr,
            __end = end_expr;
            __begin != __end; ++__begin) {
    range_declaration = *__begin;
    loop_statement
  }
}

wobei die Variablen, die mit beginnen, __nur zur Darstellung dienen begin_exprund end_exprdie Magie ist, die begin/ end.² aufruft

Die Anforderungen an den Rückgabewert für Anfang / Ende sind einfach: Sie müssen vorab überladen ++, sicherstellen, dass die Initialisierungsausdrücke gültig sind, binär !=, die in einem booleschen Kontext verwendet werden können, unär *, was etwas zurückgibt, mit dem Sie initialisieren können range_declaration, und eine Öffentlichkeit verfügbar machen Zerstörer.

Dies auf eine Weise zu tun, die nicht mit einem Iterator kompatibel ist, ist wahrscheinlich eine schlechte Idee, da zukünftige Iterationen von C ++ möglicherweise relativ unbekümmert sind, wenn Sie Ihren Code brechen, wenn Sie dies tun.

Abgesehen davon ist es ziemlich wahrscheinlich, dass eine zukünftige Überarbeitung des Standards die end_exprRückgabe eines anderen Typs als ermöglicht begin_expr. Dies ist insofern nützlich, als es eine "Lazy-End" -Auswertung (wie das Erkennen einer Nullterminierung) ermöglicht, die leicht zu optimieren ist, um so effizient wie eine handgeschriebene C-Schleife zu sein, und andere ähnliche Vorteile.


¹ Beachten Sie, dass for(:)Schleifen alle temporären auto&&Elemente in einer Variablen speichern und als l-Wert an Sie übergeben. Sie können nicht erkennen, ob Sie über einen temporären (oder einen anderen Wert) iterieren. Eine solche Überlastung wird von einer for(:)Schleife nicht aufgerufen . Siehe [stmt.ranged] 1.2-1.3 von n4527.

² entweder den Anruf begin/ eine endMethode oder ADL-only lookup freier Funktion begin/ end, oder Magie für C-style - Array - Unterstützung. Beachten Sie, dass dies std::beginnur aufgerufen wird, wenn range_expressionein Objekt vom Typ in namespace stddasselbe zurückgegeben wird oder von diesem abhängig ist.


Im Der Range-for-Ausdruck wurde aktualisiert

{
  auto && __range = range_expression ;
  auto __begin = begin_expr;
  auto __end = end_expr;
  for (;__begin != __end; ++__begin) {
    range_declaration = *__begin;
    loop_statement
  }
}

mit den Arten von __beginund __endwurden entkoppelt.

Dadurch kann der Enditerator nicht vom selben Typ wie begin sein. Ihr Enditeratortyp kann ein "Sentinel" sein, der nur !=mit dem Anfangsiteratortyp unterstützt wird.

Ein praktisches Beispiel dafür , warum dies nützlich ist , dass Ihr Ende kann Iterator „überprüfen Sie Ihre lesen , char*um zu sehen , ob es weist auf '0'“ , wenn ==mit ein char*. Auf diese Weise kann ein C ++ - Bereichsausdruck optimalen Code generieren, wenn er über einen nullterminierten char*Puffer iteriert .

struct null_sentinal_t {
  template<class Rhs,
    std::enable_if_t<!std::is_same<Rhs, null_sentinal_t>{},int> =0
  >
  friend bool operator==(Rhs const& ptr, null_sentinal_t) {
    return !*ptr;
  }
  template<class Rhs,
    std::enable_if_t<!std::is_same<Rhs, null_sentinal_t>{},int> =0
  >
  friend bool operator!=(Rhs const& ptr, null_sentinal_t) {
    return !(ptr==null_sentinal_t{});
  }
  template<class Lhs,
    std::enable_if_t<!std::is_same<Lhs, null_sentinal_t>{},int> =0
  >
  friend bool operator==(null_sentinal_t, Lhs const& ptr) {
    return !*ptr;
  }
  template<class Lhs,
    std::enable_if_t<!std::is_same<Lhs, null_sentinal_t>{},int> =0
  >
  friend bool operator!=(null_sentinal_t, Lhs const& ptr) {
    return !(null_sentinal_t{}==ptr);
  }
  friend bool operator==(null_sentinal_t, null_sentinal_t) {
    return true;
  }
  friend bool operator!=(null_sentinal_t, null_sentinal_t) {
    return false;
  }
};

Live-Beispiel in einem Compiler ohne vollständige C ++ 17-Unterstützung; forSchleife manuell erweitert.


Wenn bereichsbasiert für einen anderen Suchmechanismus verwendet, ist es möglicherweise möglich, dass bereichsbasiert für ein anderes Paar von beginund endFunktionen erhält, als dies im normalen Code verfügbar ist. Vielleicht könnten sie dann sehr spezialisiert sein, um sich anders zu verhalten (dh schneller, indem sie das Endargument ignorieren, um die größtmöglichen Optimierungsoptimierungen zu erzielen). Aber ich bin nicht gut genug mit Namespaces, um sicher zu sein, wie das geht.
Aaron McDaid

@ AaronMcDaid nicht sehr praktisch. Es würde leicht zu überraschenden Ergebnissen kommen, da einige Mittel zum Aufrufen von begin / end mit dem bereichsbezogenen Start / Ende enden würden und andere nicht. Unschuldige Änderungen (von der Client-Seite) würden zu Verhaltensänderungen führen.
Yakk - Adam Nevraumont

1
Du brauchst nicht begin(X&&). Das Temporär wird in der Luft von auto&&in einem bereichsbasierten for beginangehalten und immer mit einem lvalue ( __range) aufgerufen .
TC

2
Diese Antwort würde wirklich von einem Vorlagenbeispiel profitieren, das man kopieren und implementieren kann.
Tomáš Zato - Wiedereinsetzung Monica

Ich möchte lieber die Eigenschaften des Iteratortyps (*, ++ ,! =) Betonen. Ich sollte Sie bitten, diese Antwort neu zu formulieren, um die Spezifikationen des Iteratortyps kühner zu machen.
Red.Wave

62

Ich schreibe meine Antwort, weil einige Leute mit einfachen Beispielen aus dem wirklichen Leben ohne STL-Includes zufriedener sein könnten.

Ich habe aus irgendeinem Grund meine eigene einfache Datenarray-Implementierung und wollte den Bereich verwenden, der auf der Schleife basiert. Hier ist meine Lösung:

 template <typename DataType>
 class PodArray {
 public:
   class iterator {
   public:
     iterator(DataType * ptr): ptr(ptr){}
     iterator operator++() { ++ptr; return *this; }
     bool operator!=(const iterator & other) const { return ptr != other.ptr; }
     const DataType& operator*() const { return *ptr; }
   private:
     DataType* ptr;
   };
 private:
   unsigned len;
   DataType *val;
 public:
   iterator begin() const { return iterator(val); }
   iterator end() const { return iterator(val + len); }

   // rest of the container definition not related to the question ...
 };

Dann das Anwendungsbeispiel:

PodArray<char> array;
// fill up array in some way
for(auto& c : array)
  printf("char: %c\n", c);

2
Das Beispiel verfügt über die Methoden begin () und end () sowie eine grundlegende (leicht verständliche) Beispiel-Iteratorklasse, die problemlos für jeden benutzerdefinierten Containertyp angepasst werden kann. Der Vergleich von std :: array <> und einer möglichen alternativen Implementierung ist eine andere Frage und hat meiner Meinung nach nichts mit der bereichsbasierten for-Schleife zu tun.
csjpeter

Dies ist eine sehr präzise und praktische Antwort! Es war genau das, wonach ich gesucht habe! Vielen Dank!
Zac Taylor

1
Wäre es angemessener, das const Rückgabequalifikationsmerkmal für zu entfernen const DataType& operator*()und den Benutzer wählen zu lassen, ob const auto&oder auto&?
Rick

53

Der relevante Teil der Norm ist 6.5.4 / 1:

Wenn _RangeT ein Klassentyp ist, werden die nicht qualifizierten IDs begin und end im Bereich der Klasse _RangeT nachgeschlagen, als ob nach der Suche nach Zugriff auf Klassenmitglieder (3.4.5), und wenn einer (oder beide) mindestens eine Deklaration findet, beginnen Sie - expr und Endausdruck sind __range.begin()und __range.end()verbunden sind;

- sonst beginnen-ausdr und Endausdruck sind begin(__range)und end(__range)jeweils wo beginnen und enden mit Argument abhängigen Lookup (3.4.2) nachgeschlagen. Für die Zwecke dieser Namenssuche ist der Namespace std ein zugeordneter Namespace.

Sie können also Folgendes tun:

  • Definieren beginund endElementfunktionen
  • Definieren beginund geben Sie endFunktionen frei, die von ADL gefunden werden (vereinfachte Version: Fügen Sie sie in denselben Namespace wie die Klasse ein).
  • spezialisieren std::beginundstd::end

std::beginRuft die begin()Member-Funktion trotzdem auf. Wenn Sie also nur eine der oben genannten Funktionen implementieren, sollten die Ergebnisse unabhängig von der gewählten Funktion gleich sein. Das sind die gleichen Ergebnisse für Fernkampf-basierte for-Schleifen und das gleiche Ergebnis für bloßen Sterblichen Code, der keine eigenen Regeln für die Auflösung magischer Namen hat, also nur using std::begin;gefolgt von einem unqualifizierten Aufruf von begin(a).

Wenn Sie jedoch die Elementfunktionen und die ADL-Funktionen implementieren , sollten bereichsbasierte for-Schleifen die Elementfunktionen aufrufen, während bloße Sterbliche die ADL-Funktionen aufrufen. Stellen Sie am besten sicher, dass sie in diesem Fall dasselbe tun!

Wenn das , was Sie implementiert die Container - Schnittstelle schreiben, dann wird es haben begin()und end()Elementfunktionen bereits, was ausreichend sein sollte. Wenn es sich um einen Bereich handelt, der kein Container ist (was eine gute Idee wäre, wenn er unveränderlich ist oder wenn Sie die Größe im Voraus nicht kennen), können Sie frei wählen.

Beachten Sie, dass Sie bei den von Ihnen bereitgestellten Optionen nicht überladen dürfen std::begin(). Sie können Standardvorlagen für einen benutzerdefinierten Typ spezialisieren. Abgesehen davon ist das Hinzufügen von Definitionen zum Namespace std ein undefiniertes Verhalten. Die Spezialisierung von Standardfunktionen ist jedoch schon deshalb eine schlechte Wahl, da Sie aufgrund der fehlenden Spezialisierung auf Teilfunktionen nur für eine einzelne Klasse und nicht für eine Klassenvorlage arbeiten können.


Gibt es nicht bestimmte Anforderungen, die der Iterator viel erfüllt? dh ein ForwardIterator oder etwas in dieser Richtung sein.
Pubby

2
@Pubby: Mit Blick auf 6.5.4 denke ich, dass InputIterator ausreichend ist. Aber eigentlich glaube ich nicht , der zurückgegebene Typ hat ein Iterator sein , überhaupt für bereichsbasierte für. Die Anweisung wird im Standard durch das definiert, was sie entspricht. Es reicht also aus, nur die im Standard verwendeten Codes im Standard zu implementieren: Operatoren !=, Präfix ++und unär *. Es ist wahrscheinlich unklug zu implementieren begin()und end()Member - Funktionen oder Dritt ADL Funktionen dass die Rückkehr etwas anderes als ein Iterator, aber ich denke , dass es legal ist. std::beginIch glaube, UB ist darauf spezialisiert , einen Nicht-Iterator zurückzugeben.
Steve Jessop

Sind Sie sicher, dass Sie std :: begin nicht überladen dürfen? Ich frage, weil die Standardbibliothek dies in einigen Fällen selbst tut.
ThreeBit

@ThreeBit: Ja, ich bin sicher. Die Regeln für Standardbibliotheksimplementierungen unterscheiden sich von den Regeln für Programme.
Steve Jessop


34

Sollte ich mich nur auf begin () und end () spezialisieren?

Soweit ich weiß, ist das genug. Sie müssen auch sicherstellen, dass das Inkrementieren des Zeigers vom Anfang bis zum Ende erfolgt.

Das nächste Beispiel (es fehlt die const-Version von Anfang und Ende) wird kompiliert und funktioniert einwandfrei.

#include <iostream>
#include <algorithm>

int i=0;

struct A
{
    A()
    {
        std::generate(&v[0], &v[10], [&i](){  return ++i;} );
    }
    int * begin()
    {
        return &v[0];
    }
    int * end()
    {
        return &v[10];
    }

    int v[10];
};

int main()
{
    A a;
    for( auto it : a )
    {
        std::cout << it << std::endl;
    }
}

Hier ist ein weiteres Beispiel mit Anfang / Ende als Funktionen. Sie müssen sich aufgrund von ADL im selben Namespace wie die Klasse befinden:

#include <iostream>
#include <algorithm>


namespace foo{
int i=0;

struct A
{
    A()
    {
        std::generate(&v[0], &v[10], [&i](){  return ++i;} );
    }

    int v[10];
};

int *begin( A &v )
{
    return &v.v[0];
}
int *end( A &v )
{
    return &v.v[10];
}
} // namespace foo

int main()
{
    foo::A a;
    for( auto it : a )
    {
        std::cout << it << std::endl;
    }
}

1
@ereOn Im selben Namespace, in dem die Klasse definiert ist. Siehe das 2. Beispiel
BЈовић

2
Herzlichen Glückwunsch auch :) Es könnte erwähnenswert sein, die Begriffe Argument Dependent Lookup (ADL) oder Koenig Lookup für das zweite Beispiel zu erwähnen (um zu erklären, warum sich die freie Funktion im selben Namespace befinden sollte wie die Klasse, mit der sie arbeitet).
Matthieu M.

1
@ereOn: Eigentlich nicht. Bei ADL geht es darum, die Bereiche für die Suche so zu erweitern, dass sie automatisch die Namespaces enthalten, zu denen die Argumente gehören. Es gibt einen guten ACCU-Artikel über Überlastungsauflösung, in dem der Name-Lookup-Teil leider übersprungen wird. Die Namenssuche umfasst das Sammeln von Kandidatenfunktionen. Sie beginnen mit der Suche im aktuellen Bereich + den Bereichen der Argumente. Wenn kein übereinstimmender Name gefunden wird, wechseln Sie zum übergeordneten Bereich des aktuellen Bereichs und suchen erneut ... bis Sie den globalen Bereich erreichen.
Matthieu M.

1
@ BЈовић sorry, aber aus welchem ​​Grund geben Sie in der Funktion end () einen gefährlichen Zeiger zurück? Ich weiß, dass es funktioniert, aber ich möchte die Logik davon verstehen. Das Ende des Arrays ist v [9]. Warum sollten Sie jemals v [10] zurückgeben?
gedamial

1
@gedamial Ich stimme zu. Ich denke es sollte sein return v + 10. &v[10]dereferenziert den Speicherort direkt hinter dem Array.
Millie Smith

16

Falls Sie die Iteration einer Klasse direkt mit ihrem std::vectoroder unterstützen möchtenstd::map Mitglied unterstützen , finden Sie hier den Code dafür:

#include <iostream>
using std::cout;
using std::endl;
#include <string>
using std::string;
#include <vector>
using std::vector;
#include <map>
using std::map;


/////////////////////////////////////////////////////
/// classes
/////////////////////////////////////////////////////

class VectorValues {
private:
    vector<int> v = vector<int>(10);

public:
    vector<int>::iterator begin(){
        return v.begin();
    }
    vector<int>::iterator end(){
        return v.end();
    }
    vector<int>::const_iterator begin() const {
        return v.begin();
    }
    vector<int>::const_iterator end() const {
        return v.end();
    }
};

class MapValues {
private:
    map<string,int> v;

public:
    map<string,int>::iterator begin(){
        return v.begin();
    }
    map<string,int>::iterator end(){
        return v.end();
    }
    map<string,int>::const_iterator begin() const {
        return v.begin();
    }
    map<string,int>::const_iterator end() const {
        return v.end();
    }

    const int& operator[](string key) const {
        return v.at(key);
    }
    int& operator[](string key) {
        return v[key];
    } 
};


/////////////////////////////////////////////////////
/// main
/////////////////////////////////////////////////////

int main() {
    // VectorValues
    VectorValues items;
    int i = 0;
    for(int& item : items) {
        item = i;
        i++;
    }
    for(int& item : items)
        cout << item << " ";
    cout << endl << endl;

    // MapValues
    MapValues m;
    m["a"] = 1;
    m["b"] = 2;
    m["c"] = 3;
    for(auto pair: m)
        cout << pair.first << " " << pair.second << endl;
}

2
Erwähnenswert ist, dass const_iteratorauch auf auto(C ++ 11) -kompatible Weise über usw. zugegriffen werden cbeginkanncend
underscore_d

2

Hier teile ich das einfachste Beispiel für das Erstellen eines benutzerdefinierten Typs, der mit " bereichsbasierter for-Schleife " funktioniert :

#include<iostream>
using namespace std;

template<typename T, int sizeOfArray>
class MyCustomType
{
private:
    T *data;
    int indx;
public:
    MyCustomType(){
        data = new T[sizeOfArray];
        indx = -1;
    }
    ~MyCustomType(){
        delete []data;
    }
    void addData(T newVal){
        data[++indx] = newVal;
    }

    //write definition for begin() and end()
    //these two method will be used for "ranged based loop idiom"
    T* begin(){
        return &data[0];
    }
    T* end(){
        return  &data[sizeOfArray];
    }
};
int main()
{
    MyCustomType<double, 2> numberList;
    numberList.addData(20.25);
    numberList.addData(50.12);
    for(auto val: numberList){
        cout<<val<<endl;
    }
    return 0;
}

Hoffe, es wird für einige unerfahrene Entwickler wie mich hilfreich sein: p :)
Danke.


Warum nicht ein zusätzliches Element zuweisen, um zu vermeiden, dass ungültiger Speicher in Ihrer Endmethode dereferenziert wird?
AndersK

@Anders Da fast alle Enditeratoren nach dem Ende ihrer enthaltenen Struktur zeigen. Die end()Funktion selbst dereferenziert offensichtlich keinen falschen Speicherort, da sie nur die 'Adresse' dieses Speicherorts verwendet. Das Hinzufügen eines zusätzlichen Elements würde bedeuten, dass Sie mehr Speicher benötigen würden, und die Verwendung your_iterator::end()in einer Weise, die den Wert dereferenziert, würde ohnehin nicht mit anderen Iteratoren funktionieren, da diese auf dieselbe Weise erstellt werden.
Qqwy

@ Qqwy seine Endmethode de-refences - return &data[sizeofarray]IMHO sollte es nur die Adressdaten + Größe des Arrays zurückgeben, aber was weiß ich,
AndersK

@Anders Du bist richtig. Danke, dass du mich scharf gehalten hast :-). Ja, data + sizeofarraywäre der richtige Weg, dies zu schreiben.
Qqwy

1

Chris Redfords Antwort funktioniert (natürlich) auch für Qt-Container. Hier ist eine Anpassung (beachten Sie, dass ich constBegin()jeweils a constEnd()von den const_iterator-Methoden zurückgebe):

class MyCustomClass{
    QList<MyCustomDatatype> data_;
public:    
    // ctors,dtor, methods here...

    QList<MyCustomDatatype>::iterator begin() { return data_.begin(); }
    QList<MyCustomDatatype>::iterator end() { return data_.end(); }
    QList<MyCustomDatatype>::const_iterator begin() const{ return data_.constBegin(); }
    QList<MyCustomDatatype>::const_iterator end() const{ return data_.constEnd(); }
};

0

Ich möchte einige Teile der Antwort von @Steve Jessop näher erläutern, für die ich zunächst nichts verstanden habe. Ich hoffe es hilft.

std::beginRuft die begin()Member-Funktion trotzdem auf. Wenn Sie also nur eine der oben genannten Funktionen implementieren, sollten die Ergebnisse unabhängig von der gewählten Funktion gleich sein. Das ist das gleiche Ergebnis für Fernkampf-basierte for-Schleifen und das gleiche Ergebnis für bloßen Sterblichen Code, der keine eigenen Regeln für die Auflösung magischer Namen hat, also nur using std::begin;gefolgt von einem unqualifizierten Aufruf von begin(a).

Wenn Sie die Mitgliedsfunktionen implementieren und die ADL-Funktionen , sollten bereichsbasierte for-Schleifen die Elementfunktionen aufrufen, während bloße Sterbliche die ADL-Funktionen aufrufen. Stellen Sie am besten sicher, dass sie in diesem Fall dasselbe tun!


https://en.cppreference.com/w/cpp/language/range-for :

  • Wenn ...
  • Wenn range_expressiones sich um einen Ausdruck eines Klassentyps handelt C, bei dem sowohl ein Mitglied als auch beginein Mitglied benannt sind end(unabhängig vom Typ oder der Zugänglichkeit eines solchen Mitglieds), begin_exprist dies der Fall __range.begin( ) und end_exprist __range.end();
  • Ansonsten begin_exprist begin(__range)und end_expristend(__range) , die über eine argumentabhängige Suche gefunden werden (eine Nicht-ADL-Suche wird nicht durchgeführt).

Bei einer bereichsbasierten for-Schleife werden zuerst Elementfunktionen ausgewählt.

Aber für

using std::begin;
begin(instance);

ADL-Funktionen werden zuerst ausgewählt.


Beispiel:

#include <iostream>
#include <string>
using std::cout;
using std::endl;

namespace Foo{
    struct A{
        //member function version
        int* begin(){
            cout << "111";
            int* p = new int(3);  //leak I know, for simplicity
            return p;
        }
        int *end(){
            cout << "111";
            int* p = new int(4);
            return p;
        }
    };

    //ADL version

    int* begin(A a){
        cout << "222";
        int* p = new int(5);
        return p;
    }

    int* end(A a){
        cout << "222";
        int* p = new int(6);
        return p;
    }

}

int main(int argc, char *args[]){
//    Uncomment only one of two code sections below for each trial

//    Foo::A a;
//    using std::begin;
//    begin(a);  //ADL version are selected. If comment out ADL version, then member functions are called.


//      Foo::A a;
//      for(auto s: a){  //member functions are selected. If comment out member functions, then ADL are called.
//      }
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.