C++ ist schon eine verrückte Sprache

Gast #5313634
Lesenswert?

Ich hatte gerade etwas über Referenzen nachgelesen -- und es dann zur 
Sicherheit noch mal ausprobiert. Ist tatsächlich so wie ich es in der 
Erklärung verstanden hatte -- aber schon recht sonderbar.
1
#include <iostream>
2
int main()
3
{
4
  int i = 7;
5
  float x = 7.0;
6
  const int &ri = i;
7
  const int &rx = x;
8
  const int &ri2 = 2 * i;
9

10
  i = 8;
11
  x = 8.0;
12
  
13
  std::cout << ri << std::endl;
14
  std::cout << rx << std::endl;
15
  std::cout << ri2 << std::endl;
16
  return 0;
17
}
1
$ ./a.out 
2
8
3
7
4
14
Gast #5313661
Lesenswert?

Dass konstante Referenzen auf temporäre Werte erlaubt sind, und die 
Referenz die Lebensdauer des Werts verlängert. "rx" ist keine Referenz 
auf x (geht auch gar nicht weil inkompatible Typen), sondern eine auf 
eine "unsichtbare" float-Variable, die einmal mit dem konvertierten Wert 
von x initialisiert, aber dann nicht mehr geändert wird.
Beitrag #5313705 wurde von einem Moderator gelöscht.
Beitrag #5313708 wurde von einem Moderator gelöscht.
Beitrag #5313714 wurde von einem Moderator gelöscht.
Beitrag #5313715 wurde von einem Moderator gelöscht.
Beitrag #5313717 wurde von einem Moderator gelöscht.
Beitrag #5313730 wurde von einem Moderator gelöscht.
Gast #5313815
Lesenswert?

mh schrieb:

> Was ist daran jetzt verrückt oder sonderbar?

Nun, es spricht nicht gerade für eine Sprache, wenn nicht das 
herauskommt, was man beim ersten Betrachten des Quelltextes ganz 
natürlich erwarten würde.

Solche Sachverhalte zeigen i.d.R., das eine Sprache sinnlos aufgeblasen 
ist, bis hin zur potentiellen Unbrauchbarkeit(*).

(*) Das ist der Punkt, wenn es aufwendiger wird, die Sprache zu 
kapieren, als das Problem, was man damit lösen möchte. Dieser Punkt war 
bei C++ im Moment der Sprachschöpfung bereits in sehr vielen Fällen 
erreicht und sogar weit überschritten. Heute ist C++ reine 
Klartextverschlüsselung. Eine typische Write-only-Sprache. Es ist 
mittlerweile fast unmöglich geworden, aus dem Quelltext auf das damit 
angeblich gelöste Problem zu schliessen, selbst wenn man C++ beherrscht 
(was allerdings mittlerweile auch fast niemand mehr tatsächlich von sich 
behaupten kann)...
Gast #5313834
Lesenswert?

c-hater schrieb:
> mh schrieb:
>
>> Was ist daran jetzt verrückt oder sonderbar?
>
> Nun, es spricht nicht gerade für eine Sprache, wenn nicht das
> herauskommt, was man beim ersten Betrachten des Quelltextes ganz
> natürlich erwarten würde.
>

Das ist aber ein komplett selbst verschuldetes Problem.

Ich codiere seit >> 10 Jahren in C++, aber das ganze Gedöns aus ersten 
Beitrag in diesem thread interessiert mich nicht die Bohne. Kann auch 
(muss nicht) für Embedded komplett irrelevant sein. Ein stdout (oder 
anderer stream) ist ein künstliches Konstrukt, das mit der Sprache 
erstmal herzlich wenig zu tun hat.

Eines der Riesenvorteile von C++ ist die Möglichkeit der Einkapselung. 
Denke bei C++ an eine Hyperstrukt, die neben den (Daten)Strukturmembers 
auch Funktionen haben kann, die implizit Alles was die Instanz der 
Strukt hat mit referenziert. Also statt in C zu schreiben

Fn1(MeineStrukt *abc)
{
   ...abc->member1...
   ...
   abc->member2...
}

und

Fn2(MeineStrukt *abc)
{
   ...abc->member1...
   ...
   abc->member2...
}

schreibst Du in C++:

class MeineStrukt
{
    private <typegal> member1,member2;
    ...
    public:
    Fn1(void);
    Fn2(void);
}

MeineStrukt::Fn1()
{
   member1...
   ...
   member2...
}

MeineStrukt::Fn2()
{
   member1...
   ...
   member2...
}

Selbst (gerade) wenn Du den gesamten C++ Schnickschnak wie überladene 
Operatoren, Copy Konstruktoren, templates, SEH etc. weglässt, wird 
dieser Code gegenüber C um Millionenfach les- und wartbarer. Und kostet 
praktisch kein einziges Byte mehr footprint.

Der im TO skizzierte Code ist vielleicht für PC Programmierer 
interessant. Für Embedded gehört er zu den Klickibunti Gimmicks von C++, 
die man genau so gut weglassen kann und ohne die derselbe Code gegenüber 
C von einem wirren Wust zu richtig gut wird.
Gast #5313842
Lesenswert?

Adapter schrieb:
> Der im TO skizzierte Code ist vielleicht für PC Programmierer
> interessant. Für Embedded gehört er zu den Klickibunti Gimmicks von C++,
> die man genau so gut weglassen kann

Referenzen sind PC Klickibunti Zeug? Die Ausgabe per std::cout hat 
überhaupt nichts mit dem Problem zu tun. Würdest du per printf() 
ausgeben oder die Zahlen direkt in irgendwelche Register schreiben, wäre 
das "Problem" das gleiche.

Außerdem ist OOP nur ein kleiner Teil von C++. Da gibts noch viel mehr 
was gerade für Embedded gut ist...
Gast #5313851
Lesenswert?

Dr. Sommer schrieb:
> Adapter schrieb:
>> Der im TO skizzierte Code ist vielleicht für PC Programmierer
>> interessant. Für Embedded gehört er zu den Klickibunti Gimmicks von C++,
>> die man genau so gut weglassen kann
>
> Referenzen sind PC Klickibunti Zeug? Die Ausgabe per std::cout hat
> überhaupt nichts mit dem Problem zu tun. Würdest du per printf()
> ausgeben oder die Zahlen direkt in irgendwelche Register schreiben, wäre
> das "Problem" das gleiche.
>

Konstrukte wie std::cout haben für das, was ICH in Embedded mache, 
keinerlei Relevanz. Genau so wenig wie prontf().

> Außerdem ist OOP nur ein kleiner Teil von C++. Da gibts noch viel mehr
> was gerade für Embedded gut ist...

Beispiele?

Es ging mir darum, die im ursprünglichen Beitrag (gebetsmühlenartig) 
reiterierte Kritik von C++ als nicht mehr echtzeitles- und verstehbare 
Sprache zu relativieren und zu zeigen, dass (zumindestens für Leute aus 
der C-Welt) eine Untermenge von C++, die wirklich nichts von diesen 
Konstrukten wie << (das ist für C Programmierer ein Left shift, was 
z.Teufel soll das mit Ausgaben zu tun haben) nur Vorteile bringt.
Gast #5313858
Lesenswert?

Adapter schrieb:
> Konstrukte wie std::cout haben für das, was ICH in Embedded mache,
> keinerlei Relevanz. Genau so wenig wie prontf().

Kann sein. Hat aber nix mit dem Ausgangsposting zu tun. Ersetze das 
"std::cout<<" durch irgendeine andere Form der Ausgabe. Auch 
Embedded-Programme müssen irgendwie mit der Außenwelt kommunizieren, und 
sei es über GPIO-Pins.

Adapter schrieb:
> Beispiele?
Insbesondere Template-Metaprogrammierung und constant expressions. 
Weniger für Embedded, aber dennoch mächtig wären Exceptions 
(OOP-verwandt).

Adapter schrieb:
> Es ging mir darum, die im ursprünglichen Beitrag (gebetsmühlenartig)
> reiterierte Kritik
Die kann man einfach ignorieren.

Adapter schrieb:
> die wirklich nichts von diesen
> Konstrukten wie << (das ist für C Programmierer ein Left shift, was
> z.Teufel soll das mit Ausgaben zu tun haben) nur Vorteile bringt.
Der << Operator ist vielleicht unglücklich gewählt. Die Grundidee der 
typsicheren Ausgabe ist aber ein sehr gutes Konzept und printf() 
überlegen. Die C++ Syntax ist ohne Frage schrecklich (dank des Erbes von 
C), darüber braucht man nicht zu diskutieren, sondern kann sich der 
darunterliegenden Semantik widmen.
Gast #5313886
Lesenswert?

Vincent H. schrieb:
> Ich hab heute etwa folgendes verbrochen

Und was wird da zurückgegeben wenn NDEBUG definiert ist? Wenn das nie 
der Fall ist, warum rufst du dann nicht abort() auf, oder wirfst eine 
Exception?

Adapter schrieb:
> Hab ich noch nie gebraucht (was aber nichts heissen muss).

Die kann man für alles und jedes gebrauchen. Ganz einfaches Beispiel 
sind die Funktionen std::min/max, welches das Minimum/Maximum der 
übergebenen Zahlen zurückgeben. Sowas geht in C gar nicht (ohne 
Seiteneffekte doppelt auszuführen) und in C++ nur sinnvoll mit 
templates.
Gast #5313923
Lesenswert?

der Thread hat interessant angefangen, langsam geht es aber wieder in 
das übliche C++ bashing hier über :( Und wenn Moby den morgen früh 
findet is eh vorbei...

Das einzige was mich an dem Beispiel vom TO stört ist das der Compile 
keine Warnung wirft bei der impliziten Typkonvertierung in 'const int 
&rx = x;', jedenfalls hat mein gcc das mit den gerade aktuellen 
Einstellungen nicht gemacht. Ich habe das mal meinem µC ins Flash 
geworfen und er kommt zum selben Ergebniss wie der TO. Gut, der Code 
wurde um 230 kB grösser, aber was soll der Geiz wenn der STM32F407 512 
kB hat? Irgendwie muss man die ja mal testen :-)
#5313925
Lesenswert?

Vincent H. schrieb:
> Ja C++ erlaubt einem schon manch obskure Dinge. =)
>
> Ich hab heute etwa folgendes verbrochen
>
1
>     return []() -> R { assert(false); }();
2
>
>
> um den Return-Type einer Funktion zu befriedigen (R entspricht dem zu
> retunierenden Typen).

Hä? Was soll das darstellen? Das sollte nichtmal kompilieren, weil du 
kein return in dem Lambda hast ... das typische Konstrukt ist "return 
{};".
Moderator #5313943
Lesenswert?

Johannes S. schrieb:
> Das einzige was mich an dem Beispiel vom TO stört ist das der Compile
> keine Warnung wirft bei der impliziten Typkonvertierung in 'const int
> &rx = x;', jedenfalls hat mein gcc das mit den gerade aktuellen
> Einstellungen nicht gemacht.

Stefans Beispiel ist etwas künstlich, aber es gibt Fälle, wo bei der
Erstellung einer Referenz die implizite Typkonvertierung und das
automatische Anlegen eines temporären Objekts des neuen Types durchaus
erwünscht ist, bspw. hier:

1
void func(const std::string &s) {
2
  // mach was mit s
3
}
4

5
int main() {
6
  std::string str;
7

8
  // fülle str mit Inhalt
9

10
  func(str);   // (1)
11

12
  func("ein Stringliteral"); // (2)
13
}

Das Argument s von func ist als Referenz deklariert, damit beim Aufruf
mit einer std::string-Variable (1) diese nicht kopiert werden muss.

Beim Aufruf mit einem Stringliteral (2) passt jedoch der Datentyp nicht.
Durch die implizite Typkonvertierung in ein temporäres Objekt vom Typ
std::string funktioniert der Aufruf trotzdem. Gäbe es weder implizite
Typkonvertierungen noch automatische temporäre Objekte, müsste explizit
und umständlich eine Variable als "Überbringer" des Strings definiert
werden.
Gast #5313944
Lesenswert?

Sven B. schrieb:
> Hä? Was soll das darstellen? Das sollte nichtmal kompilieren, weil du
> kein return in dem Lambda hast ... ...

Doch das sollte kompilieren. Der Standard sagt, es ist undefined 
behaviour, wenn eine Funktion ohne richtigen Rückgabewert zurückkehrt. 
Wenn sie nie zurückkehrt ist alles in Ordnung.
Gast #5313965
Lesenswert?

Wenn wir beim Thema Referenzen sind hätte ich noch eine Frage: Ich habe 
ein Chart Objekt als übergeordnetes, das kann dann PlotAreas erstellen 
(Factory). Liefere ich diese besser als Zeiger oder als RefObjekt 
zurück? Die Idee ist die nicht wieder kaputtzumachen (kein delete), 
deshalb eher Referenzen?
Ich hatte das erst mit Zeigern gemacht, aber mit Referenzen klappt es 
auch:
1
    // basic chart object
2
    Chart chart (tft, 0, 0, 320, 200, WHITE);
3

4
    // create plot area
5
    PlotArea& plotArea = chart.addPlotArea (6 * 6, 0, 0, 10,
6
                                            tft.color565 (128, 128, 128));

habe schon länger nix mehr mit C++ gemacht, freue mich aber das man es 
jetzt so gut auf µCs nutzen kann.
Gast #5313968
Lesenswert?

Kommt drauf an, wer das PlotArea-Objekt wieder freigibt. Macht die 
"Chart"-Klasse das? Dann ist das in Ordnung so. Macht die Chart-Klasse 
das, aber verwaltet die PlotArea-Objekte intern in einem std::vector 
(welcher die auch freigeben würde)? Dann geht weder Pointer noch 
Referenz, sondern z.B. ein Index.
Soll der Aufrufer das PlotArea freigeben? Dann wäre z.B. std::unique_ptr 
sinnvoll, notfalls ein normaler Pointer, keinesfalls eine Referenz.
Gast #5313975
Lesenswert?

weil es mehrere PlotAreas per Chart werden sollen wollte ich das noch in 
einen vector packen, der kann intern aber doch Pointer auf die PlotAreas 
halten? Ob die überhaupt deleted werden sollen wäre auch zu überlegen, 
viel Dynamik möchte ich ja auf dem µC nicht, für eine GUI kann man die 
besser auf unsichtbar schalten wenn die nicht mehr dargestellt werden 
sollen.
Gast #5313976
Lesenswert?

Johannes S. schrieb:
> weil es mehrere PlotAreas per Chart werden sollen wollte ich das noch in
> einen vector packen, der kann intern aber doch Pointer auf die PlotAreas
> halten?
Wenn der vector aus Pointern besteht ist's in Ordnung. Wenn es ein 
vector<PlotArea> ist werden die Pointer ungültig wenn der vector 
verändert wird. Eventuell ist es sinnvoll vector<unique_ptr<PlotArea>> 
zu nehmen, und dann normale Pointer nach draußen zu geben.

Johannes S. schrieb:
> Ob die überhaupt deleted werden sollen wäre auch zu überlegen,
> viel Dynamik möchte ich ja auf dem µC nicht,
Steht denn zum Zeitpunkt des Kompilierens die Anzahl fest? Dann kannst 
du die auch einmalig statisch alle allokieren. Freigeben ist sinnvoll 
wenn du später weniger brauchst, aber den Speicher für irgendwas anderes 
gebrauchen kannst.
Gast #5313980
Lesenswert?

Dr. Sommer schrieb:
> Eventuell ist es sinnvoll vector<unique_ptr<PlotArea>>
> zu nehmen, und dann normale Pointer nach draußen zu geben.

ja, ich sehe schon, ich muss mir die std lib nochmal genauer ansehen, 
mit den unigue_ptr wird es am Besten sein.

Dr. Sommer schrieb:
> Steht denn zum Zeitpunkt des Kompilierens die Anzahl fest?

ich gehe da in kleinen Schritten voran und sehe mir an was das 'kostet'. 
Ein kleines Array mit fixer Anzahl ist vielleicht effizienter, aber so 
Sachen mag ich nicht wo man hinterher ellenlange Konfigs mit Konstanten 
hat. Und ein Index zu weit geschrieben ewige Fehlersuche bedeuten kann.

Danke schonmal für die Tipps, wenn es sich lohnt stelle ich den Code 
hier rein.
#5313986
Lesenswert?

mh schrieb:
> Sven B. schrieb:
>> Hä? Was soll das darstellen? Das sollte nichtmal kompilieren, weil du
>> kein return in dem Lambda hast ... ...
>
> Doch das sollte kompilieren. Der Standard sagt, es ist undefined
> behaviour, wenn eine Funktion ohne richtigen Rückgabewert zurückkehrt.
> Wenn sie nie zurückkehrt ist alles in Ordnung.

Ja naja, aber wo ist jetzt der Vorteil gegenüber return {}?
Gast #5314184
Lesenswert?

Dr. Sommer schrieb:
> Der << Operator ist vielleicht unglücklich gewählt. Die Grundidee der
> typsicheren Ausgabe ist aber ein sehr gutes Konzept und printf()
> überlegen.

Zur Ehrenrettung von printf und Konsorten sei mal angemerkt, dass 
aktuelle Compiler auch hier den Syntax prüfen und die meisten Fehler 
auch bemerken.

Für mich ist der << Operator einer der Sachen, die ich in C++ nie Nutze 
und deren Nutzen ich auch nicht nachvollziehen kann. Man kann beim Lesen 
selbst einer einfachen Ausgabezeile nicht mehr erkennen was überhaupt 
ausgegeben wird.
1
stdout << a << b << c;
Zuerst muss man halt nachsehen ob in a, b und c die "<<" Operatoren 
überladen wurden und was sie daraus machen. Man blättert dann erst mal 
durch mehrere Dateien. Und wenn dann noch jeder Programmierer denkt nur 
er hat denn Schuss gehört und seine eigenen Kreationen z.B. zum Zahlen 
Formatieren sind die besten, dann ist das unübersichtliche Chaos 
perfekt. Da ist mir ein einfaches printf() aber 1000 mal lieber und ich 
lasse mich gern in dieses Korsett zwängen. Niemand würde es z.B. gut 
finden, wenn die Formatanweisungen von printf() in jedem Programm anders 
definiert wären. Das ist aber genau die negative Konsequenz dieser 
Freiheit in C++.

Das ganze wird dann noch durch die Metaprogrammiererei mit Templates 
getoppt. Es gibt sicherlich einige Programmierer die das gut können, die 
meisten sind aber nur der Meinung dass sie das gut können. Es gibt 
genügend Beispiele hier im Forum wo aus trivialen Aufgabenstellungen 
unleserliche C++ Romane entstanden sind. Von der wunderbaren 
Debugbarkeit von Templateorgien will ich gar nicht erst reden.

Programmierer sind ja auch immer Eitel. Und aus dieser Eitelkeit heraus 
versuchen sich auch manche zu beweisen. Da wird dann jedes Problem auf 
eine Art und Weise gelöst, die man nur selbst versteht. Das vermittelt 
dann das überragende Gefühl besser zu sein als die Anderen...
#5314215
Lesenswert?

Dr. Sommer schrieb:
> Sven B. schrieb:
>> Ja naja, aber wo ist jetzt der Vorteil gegenüber return {}?
>
> return {} könnte einen Konstruktor aufrufen was noch mehr
> Programmspeicher als abort() fressen könnte
?
Der Code macht eh keinen Sinn wenn der jemals aufgerufen wird ...

> oder es könnte kein
> Standard-Konstruktor existieren.
Dann ist die Lösung delcval<R>() und nicht dieser komische Kram.
#5314234
Lesenswert?

Sven B. schrieb:
> Dann ist die Lösung delcval<R>() und nicht dieser komische Kram.

Das dacht ich zuerst auch, aber bei der libstdc++ Implementierung kommt 
dann der __declval_protector und meint "you shall not pass" ;)
1
  template<typename _Tp>
2
    struct __declval_protector
3
    {
4
      static const bool __stop = false;
5
      static typename add_rvalue_reference<_Tp>::type __delegate();
6
    };
7

8
  template<typename _Tp>
9
    inline typename add_rvalue_reference<_Tp>::type
10
    declval() noexcept
11
    {
12
      static_assert(__declval_protector<_Tp>::__stop,
13
        "declval() must not be used!");
14
      return __declval_protector<_Tp>::__delegate();
15
    }
Gast #5314236
Lesenswert?

temp schrieb:
> Für mich ist der << Operator einer der Sachen, die ich in C++ nie Nutze
> und deren Nutzen ich auch nicht nachvollziehen kann. Man kann beim Lesen
> selbst einer einfachen Ausgabezeile nicht mehr erkennen was überhaupt
> ausgegeben wird.

Das kann ich nachvollziehen. Ich selbst nutze "iostream" nur bei sehr 
sehr einfachen Sachen. Zum Beispiel wenn ich zum debuggen eine Variable 
ausgebe.

Bei allem was komplizierter ist und wo es tatsächlich auf die 
Formatierung ankommt benutze ich Alternativen, von denen es ja zum Glück 
genug gibt.

Mein Favorit ist die fmt Bibliothek (http://fmtlib.net/latest/). Die 
kann alles was ich brauche und unterstützt neben der printf auch die 
python.format Syntax. Und evtl. wird sie auch irgendwann in den c++ 
Standard integriert, zumindest ein Proposal gibt es.
Gast #5314277
Lesenswert?

Vincent H. schrieb:
> Sven B. schrieb:
>> Dann ist die Lösung delcval<R>() und nicht dieser komische Kram.
>
> Das dacht ich zuerst auch, aber bei der libstdc++ Implementierung kommt
> dann der __declval_protector und meint "you shall not pass" ;)

Wie erwartet ist die Implementierung korrekt. Der Standard sagt:
1
23.2.7 Function template declval [declval]
2
1 The library provides the function template declval to simplify the definition of expressions which occur as unevaluated operands (Clause 8).
3
template <class T> add_rvalue_reference_t<T> declval() noexcept;
4
// as unevaluated operand
5
2 Remarks: If this function is odr-used (6.2), the program is ill-formed.
6
3 Remarks: The template parameter T of declval may be an incomplete type.
7
4 [ Example:
8
template <class To, class From> decltype(static_cast<To>(declval<From>())) convert(From&&);
9
declares a function template convert which only participates in overloading if the type From can be explicitly converted to type To. For another example see class template common_type (23.15.7.6). — end example ]

Wichtig ist Remark 2.

Der Standard (C++17 Draft n4687 6.2.3) sagt zu odr-used functions:
1
... A function whose name appears as a potentially-evaluated expression is odr-used if it is the unique lookup result or the selected member of a set of overloaded functions (6.4, 16.3, 16.4), unless it is a pure virtual function and either its name is not explicitly qualified or the expression forms a pointer to member (8.3.1). [ Note: This covers calls to named functions (8.2.2), operator overloading (Clause 16), user-defined conversions (15.3.2), allocation functions for placement new-expressions (8.3.4), as well as
2
non-default initialization (11.6). A constructor selected to copy or move an object of class type is odr-used even if the call is actually elided by the implementation (15.8). — end note ] ...
Gast #5314409
Lesenswert?

temp schrieb:
> Zur Ehrenrettung von printf und Konsorten sei mal angemerkt, dass
> aktuelle Compiler auch hier den Syntax prüfen und die meisten Fehler
> auch bemerken.

Es sei denn man baut sich eigene Varianten dafür... Selbst beim 
Durchleiten mit variadischen templates funktioniert beim GCC diese 
Prüfung nicht mehr.

temp schrieb:
> Man kann beim Lesen
> selbst einer einfachen Ausgabezeile nicht mehr erkennen was überhaupt
> ausgegeben wird.
Ich finde das super. Wenn ich einen Datentyp ausgeben will, schreib ich 
"<<" hin und fertig. Ich muss mir nicht erst überlegen welche Funktionen 
dafür aufgerufen werden müssen.
Ein Beispiel:
1
#include <iostream>
2
#include <vector>
3

4
struct Point {
5
  int x, y;
6
};
7

8
std::ostream& operator << (std::ostream& os, const Point& p) {
9
  return os << "Point {" << p.x << ", " << p.y << "}";
10
}
11

12
template <typename T, typename Alloc>
13
std::ostream& operator << (std::ostream& os, const std::vector<T, Alloc>& v) {
14
  os << "{";
15
  if (!v.empty ()) {
16
    os << v.front ();
17
    for (std::size_t i = 1; i < v.size (); ++i) {
18
      os << ", " << v [i];
19
    }
20
  }
21
  return os << "}";
22
}
23

24
int main () {
25
  int i = 42;
26
  float f = 3.14;
27
  Point p { 3, 4 };
28
  
29
  std::vector<int> v1 { 1, 2, 3 };
30
  std::vector<Point> v2 { { 2, 3}, { 4, 5 }, { 6, 7} };
31
  
32
  std::cout << i << " " << f << " " << p << " " << v1 << " " << v2 << std::endl;
33
}
Der "<<" Operator wird für Point überladen, jetzt kann ich das direkt 
ausgeben. Wenn ich jetzt eine template-Klasse habe (hier: 
std::vector<T>), kann ich die enthaltenen Elemente genauso ausgeben ohne 
mir Gedanken zu machen wie bestimmte Typen jetzt ausgegeben werden 
sollen. Das wird hier genutzt um beliebige Vektoren ausgeben zu können. 
Das wäre ohne Überladen des "<<" Operators doch ziemlich umständlich. In 
der main()-Funktion kann ich dann Vektoren von int genauso ausgeben wie 
Vektoren von Point. So etwas ist super für verschachtelte generische 
Datenstrukturen.

temp schrieb:
> Zuerst muss man halt nachsehen ob in a, b und c die "<<" Operatoren
> überladen wurden und was sie daraus machen. Man blättert dann erst mal
> durch mehrere Dateien.
Das ist der Sinn von Abstraktion. Details werden verpackt, und nur 
wenn man die genau wissen will schaut man in der Implementation nach. 
Die meisten höheren Programmiersprachen nutzen das ausführlich. Wenn du 
das nicht willst, solltest du alles in Assembler programmieren und 
natürlich niemals Funktionen benutzen. Bei printf() siehst du ja auch 
nicht wie die Ausgabe jetzt genau durchgeführt wird. Du weißt es 
höchstens dank der Dokumentation von printf() - aber eigene "<<" 
Überladungen darf man auch dokumentieren.

temp schrieb:
> Das ist aber genau die negative Konsequenz dieser
> Freiheit in C++.
Man kann sich auch eigene Zahlen-Ausgaben in C mit puts() basteln. Die 
Nutzung der IO-Streams hat definitiv nichts damit zu tun ob man sich 
eigene Zahlenformate baut. Ich hab jedenfalls noch nie gesehen dass das 
jemand tun würde.

In C muss ich immer Angst haben dass die Format-Specifier von printf() 
falsch sind, sobald man den Typ der auszugebenden Variable ändert (z.B. 
time_t könnte sich von 32 auf 64bit ändern), und dann der Code nicht 
mehr funktioniert. Das kann bei den IO-Streams nicht passieren. Und ja, 
die IO-Streams bieten genau so flexible Möglichkeiten zur Formatierung 
wie printf().
Der einzige wirkliche Nachteil der IO-Streams ist dass die Strings 
zerpflückt werden, was schlecht für Übersetzungen ist.

Da ist sowas eine Lösung, was ja - genau wie IO-Streams - ebenfalls 
typsicher ist (und ebenfalls erweitert werden kann, nicht über "<<" aber 
über format_arg, mit einem ähnlichen Ergebnis):
mh schrieb:
> Mein Favorit ist die fmt Bibliothek (http://fmtlib.net/latest/).
Gast #5314427
Lesenswert?

Dr. Sommer schrieb:
> Da ist sowas eine Lösung, was ja - genau wie IO-Streams - ebenfalls
> typsicher ist (und ebenfalls erweitert werden kann, nicht über "<<" aber
> über format_arg, mit einem ähnlichen Ergebnis):
> mh schrieb:
>> Mein Favorit ist die fmt Bibliothek (http://fmtlib.net/latest/).

fmt kann auch mit ostream und operator<< für user-defined types 
erweitert werden: http://fmtlib.net/latest/api.html#std-ostream-support
Es kann also für alle Typen die schon einen operator<< für die 
std::streams haben direkt genutzt werden. Nur für Typen die eine 
parameterisierbare Formatierung benötigen muss man sich um format_arg 
kümmern.
Gast #5314495
Lesenswert?

Dr. Sommer schrieb:
> Es sei denn man baut sich eigene Varianten dafür... Selbst beim
> Durchleiten mit variadischen templates funktioniert beim GCC diese
> Prüfung nicht mehr.

Zeit für die selbsternannten C++ Gurus auch mal wieder etwas über C zu 
lernen:
https://gcc.gnu.org/onlinedocs/gcc-3.2/gcc/Function-Attributes.html
Ein einfaches Attribut sagt dem gcc dass er diese Funktion im printf 
Style überprüfen soll.
1
extern int my_printf (void *my_object, const char *my_format, ...)
2
                         __attribute__ ((format (printf, 2, 3)));

Dein Beispiel:
1
  std::cout << i << " " << f << " " << p << " " << v1 << " " << v2 << std::endl;

ist so oder ähnlich seit 30 Jahren in jedem C++ Buch zu finden und ein 
hervorragendes Beispiel für lesbaren Code... Und ich für meinen Teil 
hasse das seit genau so vielen Jahren. Du kannst es ja gut finden, das 
ändert aber nichts an meiner Meinung. Ich hab die Erfahrung gemacht, 
gepuscht wird das vor allem von Dozenten die sich an so was wie einen 
"Lehrplan" halten.
Gast #5314501
Lesenswert?

temp schrieb:
> Zeit für die selbsternannten C++ Gurus auch mal wieder etwas über C zu
> lernen:

Das Attribut kenne ich, und es ist eine Compiler-Erweiterung. Eine 
elegante Lösung ist das nicht gerade, sie basiert darauf dass der 
Compiler printf() und seine Funktionsweise kennt. Ein Compiler sollte 
normalerweise keine einzelnen Funktionen direkt unterstützen - so etwas 
sollte von innerhalb der Sprache möglich sein. Wenn man eigene 
Format-Specifier nachrüsten möchte (z.B. für eigene Datentypen) klappt 
das schon gar nicht mehr.

temp schrieb:
> ist so oder ähnlich seit 30 Jahren in jedem C++ Buch zu finden und ein
> hervorragendes Beispiel für lesbaren Code...
Wie gesagt, das "<<" ist fragwürdig. Die grundlegende Idee, Overloads zu 
nutzen, finde ich äußerst sinnvoll. Bei Arduino z.B. heißt es statt "<<" 
halt "print", mit ähnlicher Semantik. Findest du das dann besser?

temp schrieb:
> Ich hab die Erfahrung gemacht,
> gepuscht wird das vor allem von Dozenten die sich an so was wie einen
> "Lehrplan" halten.
Vielleicht kann man von Dozenten ja noch was lernen, dafür sind die 
schließlich da. Wer möchte dass der Code "hübsch" aussieht, sollte kein 
C++ nutzen. Es ist nicht schön, aber flexibler und sicherer als 
printf().
Gast #5314560
Lesenswert?

Dr. Sommer schrieb:
> Bei Arduino z.B. heißt es statt "<<"
> halt "print", mit ähnlicher Semantik. Findest du das dann besser?

Bei Arduino bestimmt aber die Print Klasse die Formattierung, beim << 
Operator wird die vom auszugebenden Objekt erstellt. Das Print müsste 
man um die neuen Typen erweitern, finde ich nicht schön.

Bezieht ihr euch bei euren iostream Beispielen auf PC oder µC? Ich hatte 
ja gestern das auf dem µC eingebunden und der Code wuchs um +230 kB 
(Debug Release mit newlib). Das erscheint mir sehr viel, ist diese 
Grössenordnung normal?
Gast #5314566
Lesenswert?

Johannes S. schrieb:
> Bei Arduino bestimmt aber die Print Klasse die Formattierung,

Nur bei vordefinierten Typen - genau wie bei io streams.

Johannes S. schrieb:
> Das Print müsste man um die neuen Typen erweitern, finde ich nicht
> schön.

Einfach einen Overload hinzufügen, genau wie bei io streams. Das ist 
absolut gängiges Vorgehen in C++, dafür gibt es ja auch ADL. Hier rächt 
sich aber das schlechte Design der Arduino Library, nämlich dass die 
Print Funktionen in einer Klasse sind...

Johannes S. schrieb:
> Bezieht ihr euch bei euren iostream Beispielen auf PC oder µC

PC. Die gängigen Implementationen der IOStreams sind nicht uC geeignet.
Gast #5314594
Lesenswert?

Dr. Sommer schrieb:
> Vielleicht kann man von Dozenten ja noch was lernen, dafür sind die
> schließlich da.

Wenn Dozenten ein simples Beispiel aufschreiben wie du:
1
int main () {
2
  int i = 42;
3
  float f = 3.14;
4
  Point p { 3, 4 };
5
  
6
  std::vector<int> v1 { 1, 2, 3 };
7
  std::vector<Point> v2 { { 2, 3}, { 4, 5 }, { 6, 7} };
8
  
9
  std::cout << i << " " << f << " " << p << " " << v1 << " " << v2 << std::endl;
10
}

Da wird ohne Not einfach ein vector benutzt der dynamischen Speicher 
belegt. Wir sind hier in einem µC Forum, also da wo es noch darum geht 
mit den Resourcen zu knausern. Die Anfänger übernehmen dass und 
programmieren in Zukunft in dem Stil weiter ohne darüber Nachzudenken 
was etwas kostet. Leider kann man auch kaum noch abschätzen was am Ende 
ein Template kostet. Deshalb macht sich auch niemand mehr Gedanken 
darüber, dafür werden die Programme immer fetter und damit auch nicht 
sicherer.

Dr. Sommer schrieb:
> Bei Arduino z.B. heißt es statt "<<"
> halt "print", mit ähnlicher Semantik. Findest du das dann besser?

Nein, Arduino ist ein ganz schlechtes Beispiel. Gerade weil es da in der 
Lib auch ziemlich inkonsequent gehandhabt wird. Wenn man da versucht 
z.B. auf dem ESP8622 ein simples Internetprotokoll (IMAP, MAIL o.ä.) mit 
Ausgaben zu Debuggen weil's anders nicht geht, kriegt man die Motten. 
Die Libs und der Core verwenden schon gemischt "\r\n" und "\n" als 
Zeilenende. Die println-Funktion nimmt std::endl. Die IMAP oder SMTP 
Strings wollen "\r\n" am Zeilenende. Am Ende ist die Ausgabe im Terminal 
immer irgendwie zerrupft.
Gast #5314613
Lesenswert?

temp schrieb:
> Da wird ohne Not einfach ein vector benutzt der dynamischen Speicher
> belegt.
Gah, das war ein Beispiel. Wenn man nur genau diese Ausgabe erreichen 
will, kann man auch
1
std::fputs("42 3.14 Point {3, 4} {1, 2, 3} {Point {2, 3}, Point {4, 5}, Point {6, 7}}", stdout);
schreiben. Manchmal hat man halt Vektoren, und das wäre eine Möglichkeit 
sie flexibel auszugeben. Tausche std::vector gegen std::array und du 
hast das gleiche in Nicht-Dynamisch.

temp schrieb:
> Leider kann man auch kaum noch abschätzen was am Ende
> ein Template kostet.
Genau so viel wie entsprechender C-Code.

temp schrieb:
> Nein, Arduino ist ein ganz schlechtes Beispiel.
Okay, lassen wir Arduino, machen wir das mal so wie im Anhang gezeigt - 
eine einfache Bibliothek zur Ausgabe von Integern und Strings. Der 
Einfachheit halber geben wir die Zahlen per printf() aus. In echt würde 
man da einen entsprechenden Algorithmus einbauen.
Bei Variante 1 haben wir Funktions-Overloads namens "print" ähnlich zu 
Arduino. Bei Variante 1.5 werden die noch etwas kompakter mit einer 
variadischen Funktion aufgerufen.
Bei Variante 2 wird wie bei IO-Streams der "<<" Operator überladen. In 
allen Versionen kann man jetzt problemlos Overloads für eigene Klassen, 
hier "Point", hinzufügen und alles über ein einheitliches Interface 
ausgeben. Welche Variante findest du jetzt besser?
Angehängte Dateien:
Moderator #5314639
Lesenswert?

Die I/O-Streams in C++ sind manchmal auf Grund ihrer Erweiterbarkeit
sehr praktisch. So kann man bspw. in der OpenCV eine Matrix ganz einfach
mit

1
  std::cout << matrix;

ausgeben.

Der absoluten Horror ist es aber, damit gewöhnliche Zahlenwerte als
sauber formatierte Tabelle auszugeben:

- Das kleinste Problem ist dabei noch der <<-Operator, der zu sperrig
  ist, nicht nur vor jeder auszugebenden Variable, sondern zusätzlich
  vor jedem Formatierungselement benötigt wird, einen Missbrauch des
  Bitshift-Operators darstellt und deswegen auch die falsche Präzedenz
  hat.

- Die Syntax ist insgesamt an Umständlichkeit und Unübersichtlichkeit
  kaum zu überbieten.

- Manche der Formatierungsparameter gelten nur jeweils für das nächste
  auszugebende Element (z.B. std::setw) und müssen deswegen u.U. laufend
  wiederholt werden.

- Andere (z.B. std::setprecision) sind persistent und müssen nach
  erfolgter Nutzung explizit rückgängig gemacht werden, um nicht andere
  Ausgaben im Programm zu beeinflussen.

Hier zwei Beispiele:

1
#include <iostream>
2
#include <iomanip>
3
#include <cstdio>
4

5
int main() {
6

7
  // Beispiel 1, Tabellezeile mit nummerierten dreidimesionalen Koordinaten
8

9
  int n =16;
10
  double x=23.2345, y=456.5732, z=-2.3456;
11

12
  std::printf("%3d %9.3f %9.3f %9.3f\n", n, x, y, z);
13

14
  std::cout << std::setw(3) << n << std::fixed << std::setprecision(3) << ' '
15
    << std::setw(9) << x << ' ' << std::setw(9) << y << ' ' << std::setw(9)
16
    << z << '\n' << std::setprecision(6) << std::defaultfloat;
17

18

19
  // Beispiel 2, Hexdumpzeile mit Adress und Low-/High-Byte
20

21
  unsigned int addr = 0x123abc;
22
  unsigned char lbyte = 0x23, hbyte=0x0a;
23

24
  printf("%08X: %02X %02X\n", addr, lbyte, hbyte);
25

26
  std::cout << std::hex << std::uppercase << std::setfill('0') << std::setw(8)
27
    << addr << ": " << std::setw(2) << +lbyte << ' ' << std::setw(2) << +hbyte
28
    << '\n' << std::setfill(' ') << std::nouppercase << std::dec;
29
}

Ausgabe:

1
 16    23.235   456.573    -2.346
2
 16    23.235   456.573    -2.346
3
00123ABC: 23 0A
4
00123ABC: 23 0A

Die Printf-Varianten sind nicht nur sehr viel kürzer, man sieht auch auf
einen Blick, welche Variablen überhaupt ausgegeben werden. Bei etwas
genauerer Betrachtung des Formatstrings kann man sich sogar vorstellen,
wie die ausgegebene Tabellenzeile ungefähr aussehen wird. Das alles geht
in der cout-Variante völlig unter.

Deswegen benutze ich I/O-Streams nur dann, wenn ich wirklich auf ihre
Erweiterbarkeit angewiesen bin. In den 95% aller Fälle, wo dies nicht
der Fall ist, ist printf für mich ganz klar die bessere Alterenative.
Beitrag #5314682 wurde von einem Moderator gelöscht.
Gast #5314801
Lesenswert?

Dr. Sommer schrieb:
> Welche Variante findest du jetzt besser?

Ziel ist es nicht eine Aufgabe zu finden für die deine Lösung besser 
passt.

Yalu X. schrieb:
> Der absoluten Horror ist es aber, damit gewöhnliche Zahlenwerte als
> sauber formatierte Tabelle auszugeben:

Dem und den ganzen anderen was du geschrieben hast kann ich voll 
zustimmen.
Es geht auch nicht darum die Ausgabe über Streams in C++ komplett zu 
verteufeln. Umgedreht kann ich aber nicht akzeptieren, dass es nur noch 
über diesen Weg gehen muss und allen, die das nicht so sehen, Ignoranz, 
Dummheit oder Lernfaulheit unterstellt wird.

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren