C++Guns – RoboBlog blogging the bot

03.04.2018

C++ Guns: sort() mit Visitor

Filed under: Allgemein — Tags: — Thomas @ 11:04

Wen hat es nicht schon einmal gestört, dass std::sort() so transparent arbeitet? Manchmal muss man wissen welche Elemente beim Sortieren wohin verschoben werden. Sozusagen eine Permutation.
Manchmal ist es möglich, den zu sortierenden Daten eine ID mitzugeben. Aber oft nicht. Zum Beispiel arbeite ich mit 2D Punkten. Diese liegen natürlich schön dicht gepackt im std::vector, und die ID ist implizit durch die Position im vector gegeben. Durch das Sortieren würde sich auch die ID ändern, was aber nicht geht, da sie mit andern Daten noch verknüpft ist. Und die ID explicit abspeichern ist auch nicht gut, da dann die x/y Koordinaten vom Punkt im Speicher nicht mehr optimal ausgerichtet sind. Das Speicherlayout ändert sich zu meinen Ungunsten.

Zumindest in meinem Anwendungsfall brauche ich die sortierten Daten auch nur kurzzeitig, und die original Daten sollen unverändert bleiben. Es würde teilweise auch vollkommen langen, wenn die sort Funktion nur ein vector mit neuen IDs in der sortierten Reihenfolge zurück gibt.

Mein erster Ansatz ist, eine sort() Funktion zu schreiben, welche als zusätzliches Argument einen Visitor akzeptiert. Diese Funktion ruft die eigentlich std::sort() Funktion auf, so dass alles vom Standard noch eingehalten wird und sich auch nichts an der asymptotischen Laufzeit ändert.
Um eine ID zu erhalten, werte ich die Adresse der Elemente im std::vector aus. Damit die original Daten (erst einmal) in ihrer ursprünglichen Reihenfolge bleiben, wird ein zweiter std::vector mit Referenz Wrappern erstellt. Dies kostet pro Element den Speicherplatz einer Referenz, also 8byte. Dies ist, denke ich, nie ein Problem.

Nach der eigentlichen Sortierung mit std::sort ist so feststellbar, wie die Elemente im vector getauscht wurden. Damit wird das Visitor Objekt aufgerufen, so dass der Anweder den Permutationsvektor erstellen kann. Und die eigentlichen Daten werden durch swap sortiert.

Hier der Code der ersten Implementierung:

template<typename T, typename Allocator, typename Visitor>
inline void sort(std::vector<T,Allocator>& data,  Visitor&& vis) {
    std::vector<std::reference_wrapper<const T>> sorted;
    sorted.reserve(data.size());
    for(const auto& x : data) {
        sorted.push_back(std::cref(x));
    }

    std::sort(sorted.begin(), sorted.end());
    // ptrdiff_t is the signed equivalent to size_t
    for(size_t i=0; i < sorted.size(); ++i) {
        // get the address of an item in data, not sorted.
        const size_t idx1 = std::addressof(sorted[i].get()) - data.data();
        if(i < idx1) {
            std::swap(data[i], data[idx1]);
            vis(i, idx1);
        }
    }
}

//////

    struct Visitor {
        void operator()(size_t i, size_t j) {
            std::cout << "Swap idx " << i << " with idx " << j << "\n";
        }
    };

    std::vector<int> v {3,2,1,0};
    cpl::sort(v, Visitor());
    QVERIFY(std::is_sorted(v.begin(), v.end()));

Swap idx 0 with idx 3
Swap idx 1 with idx 2

23.03.2018

C++ Guns: concept: force function return type

Filed under: Allgemein — Tags: — Thomas @ 02:03

Das Problem von impliziten Konversionen in C/C++ ist nicht neu. Wenn integer in floats und floats in integer kopiert werden und dabei die Werte der Zahlen verändert werden, kann das in wissenschaftlichen Anwendungen katastrophale Auswirkungen haben. Es müssen verschiedene Szenarien beachtet werden. So ist z.b. das kopieren eines 32bit integer in ein 64bit float kein Problem. Hingegen kann ein 32bit integer nicht ohne Verlust in ein 8bit integer gespeichert werden. Außer, es ist vom Algorithmus her unmöglich, dass sich die Wertebereiche überlappen.

Ich werde versuchen das Thema an Hand von Beispielen zu verdeutlichen. Sowohl für Funktions-Parameter als auch für Rückgabe Typen. Diverse Compilerparameter die man immer nutzen sollte, sowie ob Concepts hier helfen können.

Beispiel 1:
Drei Member Funktionen die jeweils ein bool übergeben, und bool zurück geben sollen. Aber der Programmierer hat sich vertan und übergibt einmal ein integer und float. Im zweiten Fall wird ein Integer zurück gegeben und im dritten Fall ist der Return Type falsch gewählt, aber es wird dennoch ein bool zurück gegeben.

Da eine implizite Umwandlung zwischen bool und integer/float erlaubt ist, erwarte ich nicht, dass der Compiler hier Fehlermeldungen wirft. Möglicherweise nur ein paar Warnings. Compiliert wird mit -Wall -Wextra -Wconversion

#include <cassert>
  
bool b_good(bool b) { 
  assert(b);
  return true;     
};

bool b_bad(bool b) { 
  assert(b);
  return 2;     
};

float b_bad2(bool b) {
  assert(b);
  return true; 
};

int main() {
  assert(b_good(true)); // okay
  assert(b_bad(4));     // bad but works. pass int ->   bool return int -> bool -> bool
  assert(b_bad2(2.0));  // bad but works. pass float -> bool return bool-> float -> bool
}

Der Compiler schweigt und das Programm läuft sauber durch. Salopp gesagt, alles was nicht 0 ist, ist wahr. Das funktioniert, aber fördert nicht die Fehlerfreiheit des Programms.

Mit narrowing conversion / brace initialization / list initialization, mit den geschweiften Klammern gibt es immerhin Warnungen bzw. gleich ein Fehler für Konstanten:

assert(b_bad({4})); 
...
return {2};

error: narrowing conversion of ‘4’ from ‘int’ to ‘bool’ inside { } [-Wnarrowing]
assert(b_bad({4}));

error: narrowing conversion of ‘2’ from ‘int’ to ‘bool’ inside { } [-Wnarrowing]
return {2};

Aber überall jetzt noch mehr Klammern einbauen kann nicht die Lösung sein. Beim return kann ich es mir noch vorstellen, aber nicht bei jedem Funktionsparameter.

Wie sieht es aus wenn statt dem Typ bool das Konzept Boolean genutzt wird? Nun, meine Variante dieses Konzept ist nicht wirklich eins, es gleicht mehr einem constraint. Ein Konzept soll ja auch möglichst vielen constrain bestehen. Aber es funktioniert. Ersetzt man die Funktionsparameter Typen durch das Konzept Boolean, kommen gute Fehlermeldungen, wie gewünscht.

template <class B> concept bool Boolean = std::is_same<B, bool>::value;

error: cannot call function ‘auto b_bad(auto:2) [with auto:2 = int]’
note: constraints not satisfied
Boolean b_bad(Boolean b) {
note: within ‘template concept const bool Boolean [with B = int]’
template concept bool Boolean = std::is_same::value;
‘std::is_same::value’ evaluated to false

error: cannot call function ‘float b_bad2(auto:3) [with auto:3 = double]’
note: constraints not satisfied
float b_bad2(Boolean b) {
note: within ‘template concept const bool Boolean [with B = double]’
template concept bool Boolean = std::is_same::value;
note: ‘std::is_same::value’ evaluated to false

#include <type_traits>

// Forcing a concept to be a fixed type. Is this a good concept or misuse?
template<typename B>
concept bool BoolType = std::is_same<B,bool>::value;

template< typename ObjectType>
concept bool ObjectInterface = requires (ObjectType obj, int a) {
  // Compound Requirements

  // does not work. 
  // 4) trailing-return-type that names a type that does not use placeholders: 
  // 4a) the type named by trailing-return-type is valid (type constraint)
  // 4b) the result of the expression is implicitly convertible to that type (implicit conversion constraint)
  // {obj.u1(a)} -> bool;  
  
  // works
  // 3) trailing-return-type that names a type that uses placeholders,
  // the type must be deducible from the type of the expression (argument deduction constraint)  
  {obj.u1(a)} -> BoolType;
};

void f(ObjectInterface obj) {
  obj.u1(1);
}

struct MyType {
  auto u1(int a) { return false; }
//  auto u1(int a) { return 1; } // error
};

int main () {
  f(MyType());
}

concept.cpp: In function ‘int main()’:
concept.cpp:30:13: error: cannot call function ‘void f(auto:1) [with auto:1 = MyType]’
f(MyType());
^
concept.cpp:21:6: note: constraints not satisfied
void f(ObjectInterface obj) {
^
concept.cpp:8:14: note: within ‘template concept const bool ObjectInterface [with ObjectType = MyType]’
concept bool ObjectInterface = requires (ObjectType obj, int a) {
^~~~~~~~~~~~~~~
concept.cpp:8:14: note: with ‘MyType obj’
concept.cpp:8:14: note: with ‘int a’
concept.cpp:8:14: note: unable to deduce placeholder type ‘BoolType’ from ‘obj.MyType::u1(a)’

15.03.2018

C++ Guns: Quick Q: What is a non-trivial constructor in C++?

Filed under: Allgemein — Tags: — Thomas @ 09:03

https://isocpp.org/blog/2018/03/quick-q-what-is-a-non-trivial-constructor-in-cpp

Super und kurze Zusammenfassung was triviale Konstruktoren sind und wie man sie bekommt.
Es zeigt sich, dass man für reine Datentypen besser auf viele lustige c++ Features verzichten soll.
Wenn man einfach _garnichts_ programmiert und nur seine member typen deklariert, ja noch nicht mal default initialisieren, steht man am besten da.
Auf einmal wird der Datentyp trivial und alles passiert automatisch weil es eben so einfach ist, dass der Compiler für dich den Rest programmieren kann.

27.02.2018

C++ Guns: Templated class specialization where template argument is a template

Filed under: Allgemein — Tags: — Thomas @ 16:02

Okay das ist mit Abstand das krasseste was ich seit langem mit Templates gemacht habe, obwohl die Aufgabe doch recht klar und einfach ist. Der Code ist von der Syntax her auch noch irgendwo okay. Naja, vllt. auch nicht.

Ich habe meine Experimentelle Graph Klasse mit entsprechenden Iteratoren. Datencontainer und Algorithmus trennen und mit Iteratoren wieder verbinden klappt btw. ziemlich gut. Da hat er 1984 was richtig gemacht.

Seit C++17 gibt es std::distance(first, last) welche die Anzahl der Elemente zwischen first und last zurück gibt. Die möchte ich nutzen. Dazu müssen meine Iteratoren std kompatibel werden. Genauer gesagt müssen die Anforderungen an einem InputIterator erfüllt werden. Hierfür muss man std::iterator_traits spezialisieren. Und die 5 Variablen difference_type, value_type, pointer, reference, iterator_category erstellen und auch hoffentlich richtig belegen.

Funktionen im std namespace zu überladen, oder template zu spezialisieren ist in der Regel nicht erlaubt, aber bei std::iterator_trais explizit gewünscht. Man muss nur darauf achten den Code nicht in irgendeinem Namespace zu schreiben, sonst wird er vom Compiler nicht gefunden.

#include <iterator>

template<>
struct std::iterator_traits<MyIterator> {
    using Iterator          = ...;
    using difference_type   = ...;
    using value_type        = ...;
    using pointer           = ...;
    using reference         = ...;
    using iterator_category = ...;
};

So weit so einfach, dumm nur, wenn die Klasse MyIterator wiederrum eine Template Klasse ist. Also braucht man eine Template Klassen Spezialisierung mit einer Template Klasse. Okay das ist schon halb WTF. Wir wollen also ein Template spezialisieren, so dass die Template Parameter gesetzt sind, aber die Parameter sind selbst wieder Template. Die Spezialisierung ist also garnicht speziell, nur ein bisschen.

Wenn man am Ende noch an das Keyword typename denkt (der Compiler erinnert einem gut daran), da wir ja alles vertemplated haben, schaut es am Ende so aus:

template<typename Graph>
struct std::iterator_traits<cpl::graph::ConnNodeIterator<Graph>> {
    using Iterator = cpl::graph::ConnNodeIterator<Graph>;
    using difference_type   = typename Iterator::difference_type;
    using value_type        = typename Iterator::value_type;
    using pointer           = typename Iterator::pointer;
    using reference         = typename Iterator::reference;
    using iterator_category = typename Iterator::iterator_category;
};

Update: Habe nochmal nachgeschaut. Eine Template Spezialisierung beginnt normalerweise mit template<>. Aber in diesem Fall muss man das template<> mit Parameter füllen. Die Spezialisierung funktioniert dennoch :D

Alles nur damit ich das hier so bequem schreiben kann:

NodeID ID = 0;
graph.connNodeRange(ID).size();

11.02.2018

C++ Guns: std::chrono::duration to nice human readable text

Filed under: Allgemein — Tags: — Thomas @ 13:02

std::chrono::duration stellt eine Zeitspanne dar. Zum Beispiel wie lange ein Programm schon läuft. Eine Textausgabe in Sekunde, Minuten, Stunden u.s.w wäre also schön. Die Umrechnung von std::chrono::duration nach Stunden ist super einfach.
Die Hauptarbeit übernimmt std::chrono::duration_cast und vordefinierte Werte für Sekunden, Minunten und Stunden gibt es schon. Die Erweiterung für Tage ist entsprechend einfach.

void nicedateTime(std::chrono::duration<double> dur) {
  typedef std::chrono::duration<int64_t, std::ratio<24*3600>> day;

  auto days = std::chrono::duration_cast<day>(dur);
  dur -= days;
  std::cout << days.count() << "d ";

  auto HH = std::chrono::duration_cast<std::chrono::hours>(dur);
  dur -= HH;
  std::cout << HH.count() << "h ";

  auto min = std::chrono::duration_cast<std::chrono::minutes>(dur);
  dur -= min;
  std::cout << min.count() << "m ";

  auto sec = std::chrono::duration_cast<std::chrono::seconds>(dur);
  std::cout << sec.count() << "s ";
}

05.02.2018

C++ Guns: Generic Data Type Design Pattern - Teil 4

Filed under: Allgemein — Tags: — Thomas @ 16:02

Die Idee, für User defined Datentypen jede Variable in einem std::tuple oder std::array zu halten, kann nicht überall gut sein. Für jede Variable benötigt man noch eine getter Funktion um der Variablen einen Namen und damit Semantik zu geben. Ich hatte in den letzten Kapitel noch erwähnt, dass es viel Sinn macht, diese Funktionen als const zu deklarieren. Da in den Beispiel Datentypen jede Variablen von der anderen abhängt. Und daher dürfen sie nicht beliebig getrennt verändert werden.

Wann diese Regel nicht mehr gilt ist ganz klar: Wenn Variablen in einem Typ gespeichert werden, die nicht voneinander abhängen. Da frage ich mich natürlich gleich, warum sie dann in einem gemeinsamen Typ gespeichert sind. Aber womöglich ist dieser Übergang ja fließend.

Sowas wie PointReader/Writer hätte als Membervariable natürlich den Path zur Datei, bzw. irgendeine Art von Device/File Handle. Eventuell noch eine Error Variable. Diese beiden Variablen sind natürlich nicht so streng gekoppelt wie xyz, aber gehören dennoch zusammen.

Eigentlich sind das ja Klassen die Verhalten simulieren. Sowas wie ein Widget. Mit Fenstergröße, Position, Farbe, Childwidgets.

Also ich halte mal fest, für Membervaribalen std::tuple std::array nutzen erscheint mir für Massendaten sinnvoll. Sonst nicht. Mal sehn ob sich das in der Praxis bewährt.

02.02.2018

C++ Guns: Generic Data Type Design Pattern - Teil 3

Filed under: Allgemein — Tags: — Thomas @ 23:02

Im letzten Teil hatte ich das serialisieren angesprochen wie das z.B. mit der Qt Klasse (und andere) QDataStream möglich ist.

A data stream is a binary stream of encoded information which is 100% independent of the host computer's operating system, CPU or byte order. For example, a data stream that is written by a PC under Windows can be read by a Sun SPARC running Solaris.

Da nach dem angesprochenen Entwurfsmuster jeder Datentyp von std::array oder std::tuple erbst, braucht es auch nur eine Serialisierungsfunktion für alle User defined Typen. Es fehlt nur eine Möglichkeit den Klassennamen automatisiert anzugeben. So ein bissel C++ reflektion fehlt leider, aber es sollte kein Problem sein den Klassennamen selbst hin zu schreiben,

strcut UserType : public std::tuple<int, double, std::string>
{
  static const char* classname = "UserType";
}

So hätte man eine Möglichkeit, User defined Datentypen automatisiert zu verarbeiten, da sie alle das selbe Interface haben. (Vom Typ std::array oder std::tuple sind).
Ein netter Gedanke.
Aber ich schweife etwas ab. Im vierten Teil versuche ich ein Anwendungsfall zu finden, bei dem dieses Muster nicht mehr passt.

C++ Guns: Generic Data Type Design Pattern - Teil 2

Filed under: Allgemein — Tags: — Thomas @ 16:02

Weiter gehts mit der Anwendung meiner Idee alle einfachen Datentypen als std::array oder std::tuple abzubilden. Wie im ersten Teil schon für den Point2D Datentyp realisiert.

Jetzt werde ich einen Range Datentyp bauen. Dieser besteht einfach aus zwei Zahlen die eine obere und untere Grenze von etwas angibt. Zusätzlich ist die Spanne zwischen obere und untere Grenze interessant. Dies ist aber eine abgeleitete Größe und darf deshalb nicht als gespeichert werden, sondern soll extra berechnet werden.

struct Range : public std::array<double,2>
{
  const auto& lower() const {
    return operator[](0);
  }

  const auto& upper() const {
    return operator[](1);
  }

  const auto size() const {
    return upper()-lower();
  }
}

So, die Intitialisierung erfolgt über eine Liste. Der Zugriff einheitlich über Funktionen. Sowohl für gespeicherte Werte sowie für abgeleitete. Das ist schon mal gut.

Hässliche Member Variablen fallen mit diesem Entwurfsmuster weg. Damit muss man sich und auch nicht mehr überlegen wie man sie nennt. Mit Unterstrich, Groß/Kleinschreibung oder ganz kryptisch und verkrüppelt. Nein, die Entscheidung muss überhaupt nicht mehr gefällt werden.

Da dieses Beispiel doch recht einfach war, versuchen wir es doch mal mit den Stochastischen Momenten.
Also einen Datentyp welche Mittelwert, Varianz und Standardabweichung bereitstellt. Nun kann man aus einer vorher erstellten Summe und der Anzahl der Summanden diese drei Werte immer berechnen. Aber das erscheint mir zu aufwendig und irgendwie nicht richtig. Daher tendiere ich dazu, avg var und std direkt in dem Datentyp abzuspeichern.

Versuchen wir es:

struct StochastikMoment : public std::array<3,double>
{
  template<typename Container>
  StochastikMoment(const Container& data) {
    operator[0] = sum(data)/data.size();
    operator[1] = ...;
    operator[2] = std::sqrt(operator[1]);
  }

  const auto& avg() const {
    return operator[0];
  }

  const auto& var() const {
    return operator[1];
  }

  const auto& std() const {
    return operator[2];
  }
}

Die Initialisierung kann wieder über eine Liste erfolgen, wenn nötig. Aber im Normalfall werden die Momente aus vorgegebenen Daten berechnet. Den Algorithmus dafür spare ich mir.
Der Zugriff Erfolg wie gewohnt über Funktionen. Ich denke an diesem Beispiel wird klar, warum nur const Funktionen in diesem Entwurfsmuster Sinn machen. Die Standardabweichung ist streng verbunden mit der Varianz. Und die Varianz hängt stark vom Mittelwert ab. Es macht einfach keinen Sinn, Mittelwert und Varianz algorithmus zu bestimmen aber die Varianz manuell vorzugegen. Diese drei Werte hängen voneinander ab und treten dementsprechend immer zusammen auf.

Hätte ich mich bei diesem Datentyp anders entschieden und die drei Werte immer wieder berechnet, und die Summe gespeichert, dann wäre das doch auch ganz klar. Wenn man den Mittelwert berechnet, kann man ihn nicht über avg() Setzen.

Ich frage mich ob diese Art und Weise allgemeingültig für alle einfachen Datentypen ist.

Auf jeden Fall ist das serialisieren dadurch einfach. Weiter im dritten Teil.

C++ Guns: Generic Data Type Design Pattern - Teil 1

Filed under: Allgemein — Tags: — Thomas @ 15:02

Hmm das Problem lässt sich vermutlich am besten anhand von Beispielen erklären.

Wie immer nehm ich gern mein Point2D Datentyp her. Es gibt ja verschiedene Wege so etwas zu implementieren.
Eine class mit privaten x/y Variablen und entsprechende getter/setter. Oder ein struct mit nur public x/y Variablen:

strcut Point2D {
 double x, y;
}

Das Problem bei dieser Implementierung ist, dass die x/y Variablen nicht in einem Array sind. Das mag vllt. auf den ersten Blick unnötigt erscheinen, aber im laufe der Jahre habe ich den Zugriff auf xyz anhand von Indizes 1,2,3 öfter mal gebraucht. Die Alternative wäre also:

strcut Point2D {
  std::array<double,2> xy;
}

Aber hier fehlt dann die Möglichkeit gut auszudrücken, dass man explizit auf die x-Variable zugreifen möchte. Wie schnell hat man doch die Index Zahlen verdreht. Eine Kombination beider Variablen ist auch Möglich:

strcut Point2D {
  std::array<double,2> xy;

  auto& x() {
    return operator[](0);
  }

  auto& y() {
    return operator[](1);
  }
}

Die const Varianten der x() y() habe ich jetzt mal weg gelassen. Ich möchte jetzt auf etwas anderes hinweisen: Wie sieht es aus wenn man dieses Konstrukt in seinem Code nutzt:

  Point2D p;
  p[0] = 10;
  p[1] = 20
  cout << p.x() << p.y();
  p.xy[0] = 100;
  p.xy[1] = 200;
  p.xy = {3,4}; ???
  cout << p. xy; ???
  p.x() = 1000;
  p.y() = 2000;

Also ganz ehrlich: Nein. Das ist zu viel des Guten. Zu viele Freiheitsgrade. Das verwirrt doch nur. Und wer die Qual der Wahl hat, trifft die falsche Entscheidung. Vor allem, muss man wirklich die Koordinaten über die Funktion x() und y() setzten können? Das sieht auch ungewohnt aus. Und die extra Variable xy erscheint auch unnötig und überflüssig. Zugriff auf Member, über Funktionen, über Index Operator. Uneinheitlich!

Vor ein paar Monaten bin ich ja auf noch eine weitere Möglichkeit gestoßen, diesen Datentyp zu implementieren. Siehe C++ Guns - C++17 Extension to aggregate initialization. Und ich muss sagen, so schlecht ist das gar nicht. Es bietet eine Menge.

Die Idee ist, den Point2D Datentyp von einem std::array erben zu lassen. Und für die bessere Lesbarkeit noch getter Funktionen mit guten Namen bereit zu stellen. Durch die Vererbung IST der Point2D Datentyp ein Array. Und bietet automatisch den Index Zugriff an. Und durch die getter Funktionen bekommen die einzelnen Arraypositionen eine Semantik.

strcut Point2D : public std::array<double,2>
{
  const auto& x() const {
    return operator[](0);
  }

  const auto& y() const {
    return operator[](1);
  }
}

Nun gestaltet sich die Nutzung schon einfacher. Das Setzten der Werte passiert über eine Initialisierungsliste. Das Lesen über Funktionen. Und in den Fällen wo ein Indexzugriff besser ist, ist das nun auch realisierbar.

  Point2D p {1,2};
  cout << p.x() << p.y();
  p[0] = p[1];

Hier habe explicit nur const getter Benutzt. Dazu sage ich später noch mehr.

Im nächsten Teil werde ich Versuchen, dieses Muster auf andere Datentypen anzuwenden.

23.01.2018

C++ Guns - C++17 class template argument deduction user guide

Filed under: Allgemein — Tags: — Thomas @ 16:01

Es ist ja bekannt, dass der Compiler die Template Parameter von Funktionen selbst bestimmen kann.

template<typename T>
T func(T val) {
  return val;
}

auto i = func(1); // i int

Seit C++17 funktioniert das nun auch mit Klassen. Das Feature hierfür nennt sich __cpp_deduction_guides und wird seit GCC 7 unterstützt.
https://en.cppreference.com/w/cpp/language/class_template_argument_deduction
Mehr noch, es können User Guides angegeben werden. Mit denen wird festgelegt, wie genau sich der Type der Klasse ableiten soll.
Hier ein einfaches Beispiel:

#include <type_traits>

#ifndef __cpp_deduction_guides
#error "need gcc 7"
#endif

template<class T>
struct A {
    using value_type = T;

    template<typename U, typename V>
    constexpr A(U, V) {
    }
};

// Das ist der  class template argument deduction user guide
// Der gemeinsame Type von U und V soll T von der Klasse A sein.
template<typename U, typename V>
A(U, V) -> A<std::common_type_t<U, V>>;


constexpr A x(1, 2.0);
// Der gemeinsame Type von int und double ist double
// Zur Compilezeit getestet!
static_assert(std::is_same_v<decltype(x)::value_type, double>);
// yeah

Sehr geiles feature.

« Newer PostsOlder Posts »

Powered by WordPress