const bei Variablen als Funktionsparameter



  • Morgen zusammen,

    ich stelle mir die Frage:
    😕 Wäre es korrekt Funktionsparameter als const zu deklarieren, wenn sie im Rumpf nicht mehr geändert werden (es geht nicht um Pointer oder Referenzen)?

    Als Beispiel mal eine einfache Multiplikation.:

    void multi(int var__) 
    {
        return var__*var__;
    }
    

    Eigentlich wäre korrekt:

    void multi(int const var__) 
    {
        return var__*var__;
    }
    

    Bei Variablen im Rumpf geht man schliesslich auch folgend vor.

    void multi_multi(int var__) 
    {
        int const multi_result{var__*var__};
        return  multi_result* multi_result;
    }
    

    Würde die const-Deklarierung bei Funktionsargumenten den Compiler zu weiteren Optimierungen anweisen?
    Oder ist es einfach nur unnötig?

    Ich würde meinen, dass ich die const-Funktionsargumentschreibweise nie in anderem Code sehe.

    Gruß
    Tommy



  • Würde die const-Deklarierung bei Funktionsargumenten den Compiler zu weiteren Optimierungen anweisen?

    Nö.
    Dieselbe Frage habe ich auch mal gestellt, und bekam als Antwort, dass man so wenig wie möglich über die Implementierung der Funktion nach außen geben sollte - sei es durch die Typen von Funktionsargumenten o. ä.
    Also, Parameter const machen schön lassen. Es bringt keinen Vorteil, gerade nicht bei solch einem Einzeiler.



  • Einen Mehrwert hat das nicht. Da die ursprünglichen Variablen ja eh nicht geändert werden da Call-By-Value.

    Auf der anderen Seite erwartet man, dass du deine Funktion beim schreiben so gut kennst und Bescheid weisst, dass du konstante Werte nicht neuzuweist.

    const hat ja in sehr vielen Fällen keine Auswirkungen auf den Maschinencode (die Ausnahmen sind wirkliche Konstanten oder (String-)Literale, diese Konstanten werden in einem speziellen Speicherbereich im Programm gespeichert oder sogar ge-"inlint"). const ist dafür da, dass du dem Compiler sagst, dass er dich vor Fehlern bewahren soll.

    Das const bei Call-by-Value-Parametern von Funktionen wird weg-"optimiert"



  • Ok, danke für eure Antworten.

    Und wie steht es, wenn das Argument als Schleifenbedingung verwendet wird?

    void loop(int var__) 
    { 
        for(int i{0} ; i != var__ ; ++i)
            ; // do something
    }
    

    var__ könnte sich ja ändern 🙂

    Wäre dann besser?

    void loop(int var__) 
    { 
        const int condition{var__};
        for(int i{0} ; i != condition ; ++i)
            ; // do something
    }
    

    Danke!



  • Machen kannst du das natürlich, ist auch nicht so schlecht.

    Ja nachdem verlierst du ein wenig Flexibilität:

    void foo()
    {
    	vector<int> vec;
    	std::generate_n(std::back_inserter(vec), 10, [](){static int i = 0; return i++;});
    
    	const std::size_t size = vec.size(); // = 10
    	for(unsigned int i = 0; i < size; ++i)
    	{
    		if ( vec[i]%2 == 0 )
    		{
    			vec.erase(vec.begin()+i);
    			--i;
    		}
    	}
    }
    

    Um mal ein hypothetisches, pathologisches Beispiel zu nennen.



  • tommy_tom_tom schrieb:

    Und wie steht es, wenn das Argument als Schleifenbedingung verwendet wird?

    void loop(int var__) 
    { 
        for(int i{0} ; i != var__ ; ++i)
            ; // do something
    }
    

    var__ könnte sich ja ändern 🙂

    Wäre dann besser?

    void loop(int var__) 
    { 
        const int condition{var__};
        for(int i{0} ; i != condition ; ++i)
            ; // do something
    }
    

    Also wenn die Funktion so klein ist, dass ich mit einem Blick alles übersehe, was da passiert, dann kümmere ich mich nicht weiter um das const. Wenn die Funktion aber bissel größer ist, dann mache ich eine const-ref auf den Parameter.

    void func(int n) // n should be const
    {
       const int& cn = n;
       // use cn as of now
    }
    


  • Hm,

    tommy_tom_tom schrieb:

    void loop(int var__) 
    { 
        const int condition{var__};
        for(int i{0} ; i != condition ; ++i)
            ; // do something
    }
    

    Skym0sh0 schrieb:

    void foo()
    {
    	vector<int> vec;
    	std::generate_n(std::back_inserter(vec), 10, [](){static int i = 0; return i++;});
    	
    	const std::size_t size = vec.size(); // = 10
    	for(unsigned int i = 0; i < size; ++i)
    	{
    		if ( vec[i]%2 == 0 )
    		{
    			vec.erase(vec.begin()+i);
    			--i;
    		}
    	}
    }
    

    wären denn die resultierenden Schleifen mit den zusätzlichen const Variablen
    als Bedingung performanter oder kosteten die zusätzlichen Variablen nur mehr?

    Ich sollte vielleicht einen Test schreiben 🙂

    out schrieb:

    void func(int n) // n should be const
    {
       const int& cn = n;
       // use cn as of now
    }
    

    Ist die Reference auf int performanter als eine Kopie? Wäre nicht ein Kopie besser bei kleinen Datentypen?

    ACHSO:

    tommy_tom_tom schrieb:

    void multi(int var__) 
    {
        return var__*var__;
    }
    
    void multi(int const var__) 
    {
        return var__*var__;
    }
    
    void multi_multi(int var__) 
    {
        int const multi_result{var__*var__};
        return  multi_result* multi_result;
    }
    

    Der Korrektheit wegens:

    int /* void */ multi(int var__) 
    {
        return var__*var__;
    }
    
    int /* void */ multi(int const var__) 
    {
        return var__*var__;
    }
    
    int /* void */ multi_multi(int var__) 
    {
        int const multi_result{var__*var__};
        return  multi_result* multi_result;
    }
    


  • ...



  • Sone schrieb:

    Dieselbe Frage habe ich auch mal gestellt, und bekam als Antwort, dass man so wenig wie möglich über die Implementierung der Funktion nach außen geben sollte - sei es durch die Typen von Funktionsargumenten o. ä.
    Also, Parameter const machen schön lassen. Es bringt keinen Vorteil, gerade nicht bei solch einem Einzeiler.

    Ich sehe hier keinen Zusammenhang zwischen dem ersten und dem zweiten Teil deiner Antwort. Ich hoffe, du weißt, dass man

    header

    int foo(int a, int b);
    

    cpp

    int foo(const int a, const int b)
    {
      return a+b;
    }
    

    schreiben kann. Die Signaturen sind äquivalent, da das const auf dem obersten Level (und nur dort) quasi ignoriert wird. Der einzige Unterschied ist hier bei der const-Version innerhalb der Implementierung, dass a und b konstante Lvalues sind. Dem Aufrufer ist das völlig egal, weil a und b funktionslokale Variablen sind, an die er eh nicht ohne weiteres rankommt.

    @OP: Ich denke nicht, dass das const hier mehr Optimierung erlaubt. Es ist nur eine Möglichkeit, dem Compiler zu sagen, dass wenn man versehentlich eine solche Variable zu ändern versucht, dass das dann einen Compile-Fehler provoziert.



  • Man müsste Mal benchmarken, ob bei einer for-Schleife mit Bedingung auf die Containergröße dieselbe wegoptimiert wird, wenn sie sich nicht ändert.

    Ich habe jedenfalls in meiner Rechen-Engine eine Schleife, die schnell sein muss und ungefähr 1 Mio. Mal pro Sekunde aufgerufen wird. Und ob ich jetzt das Attribut der Vektorgröße abfrage oder nicht, fällt da überhaupt nicht ins Gewicht. Vermutlich wird die Größe intern doch sowieso in irgendein Register geschoben.

    Wenn Deine Funktion aber noch performancekritischer und dennoch nicht so trivial ist, dass man die Schleife durch eine nicht-iterative Variante ersetzen kann, würde ich es einfach Mal disassemblieren und schauen, was passiert. Oder eben wie gesagt benchmarken.



  • Swordfish schrieb:

    tommy_tom_tom schrieb:

    Ist die Reference auf int performanter als eine Kopie? Wäre nicht ein Kopie besser bei kleinen Datentypen?

    Richtig. Nimm Bei Typen bis nicht viel größer als ein Pointer keine Zeiger/Referenzen.

    Wenn ich innerhalb eines Scopes ein Objekt erzeuge und im selben Scope eine Referenz auf dieses Objekt definiere... da hoffe ich doch, dass es dann wirklich nur ein Aliasname gibt, und da nicht intern irgendwie ein Zeiger erstellt wird und der dann jedesmal dereferenziert wird. Ich gehe davon aus, dass es bei einer Paramterübergabe eventuell so ist, aber nicht wenn ich mich im selben Scope befinde. Oder?



  • ...



  • Swordfish schrieb:

    Sone schrieb:

    [...] und bekam als Antwort, dass man so wenig wie möglich über die Implementierung der Funktion nach außen geben sollte - sei es durch die Typen von Funktionsargumenten o. ä.

    Welche Information hast du bei

    int foo( int const bar );
    

    mehr über die Implementierung als bei

    int foo( int bar );
    

    Außer, dass du weißt, das ich den Parameter bar in der Funktion nur lesen werde? Was ist daran phöse?

    Keine Ahnung. Ich stimme dir zu. Nur lehrt die Erfahrung, dass ich lieber das Denken sein lassen sollte, und auf die Pros hören, und da Shade of Mine das gesagt hat... tjo...

    Swordfish schrieb:

    Sone schrieb:

    Also, Parameter const machen schön lassen. Es bringt keinen Vorteil, gerade nicht bei solch einem Einzeiler.

    Es hat zumindest einen Vorteil: Sollte ich aus versehen einen Parameter innerhalb der Funktion ändern wollen, macht mich durch const der Compiler darauf aufmerksam.

    Ja.. na wie gesagt, s. o.



  • krümelkacker schrieb:

    Ich hoffe, du weißt, dass man

    header

    int foo(int a, int b);
    

    cpp

    int foo(const int a, const int b)
    {
      return a+b;
    }
    

    schreiben kann. Die Signaturen sind äquivalent, da das const auf dem obersten Level (und nur dort) quasi ignoriert wird. Der einzige Unterschied ist hier bei der const-Version innerhalb der Implementierung, dass a und b konstante Lvalues sind. Dem Aufrufer ist das völlig egal, weil a und b funktionslokale Variablen sind, an die er eh nicht ohne weiteres rankommt.

    Nein, das wusste ich in der Tat nicht.
    Das ist ja einfach geil. 😮 👍


  • Mod

    krümelkacker schrieb:

    Ich hoffe, du weißt, dass man

    header

    int foo(int a, int b);
    

    cpp

    int foo(const int a, const int b)
    {
      return a+b;
    }
    

    schreiben kann. Die Signaturen sind äquivalent, da das const auf dem obersten Level (und nur dort) quasi ignoriert wird. Der einzige Unterschied ist hier bei der const-Version innerhalb der Implementierung, dass a und b konstante Lvalues sind. Dem Aufrufer ist das völlig egal, weil a und b funktionslokale Variablen sind, an die er eh nicht ohne weiteres rankommt.

    Nebenbei bemerkt gilt dise Transformation ebenso (wie array->pointer und function->pointer) nur für Funktionsparameter.

    const void foo();
    void foo();
    

    sind unterschiedliche Funktionen (und können nat. auch nicht so überladen werden).

    krümelkacker schrieb:

    @OP: Ich denke nicht, dass das const hier mehr Optimierung erlaubt. Es ist nur eine Möglichkeit, dem Compiler zu sagen, dass wenn man versehentlich eine solche Variable zu ändern versucht, dass das dann einen Compile-Fehler provoziert.

    Man kann ggf. etwas konstruieren wie

    void opaque(const int&);
    void for_each(const int first, const int last)
    {
        for ( ; first != last; ++first )
            opaque(last);
    }
    

    Das sind aber schon fast pathologische Fälle.



  • tommy_tom_tom schrieb:

    void multi(int var__) 
    {
        return var__*var__;
    }
    

    Bezeichner mit doppelten Unterstrichen sind reserviert. Das ist also undefiniertes Verhalten (wenn der Rückgabetyp stimmen würde).

    Zum Thema: Dem Compiler ist es egal ob bei einem Parameter const steht oder nicht. Ob die Variable verändert wird, kann er beim Optimieren selbst feststellen.
    Für den menschlichen Leser wäre es aber interessant schnell zu erkennen, ob die Variable in der Funktion jemals verändert wird. Da die wenigsten Argumente in der Praxis verändert werden, wäre ein implizites const sinnvoll ( mutable für nicht- const ). Damit müsste man die Parameterliste der Deklaration nicht jedes Mal für die Definition anpassen. Da das Anpassen nervig und zeitaufwendig ist, macht das fast niemand.
    Aus historischen Gründen kann man natürlich nicht die Sprache so verändern.
    Ich könnte mir aber vorstellen, dieses Verhalten für Teile der Übersetzungseinheit zu aktivieren.

    #if __cplusplus >= 204202L
    #	define MUTABLE mutable
    #else
    #	define MUTABLE
    #endif
    
    #pragma push_default_const_arguments(1)
    
    int inc(int a)
    {
    	++a; //ill-formed, weil a implizit ein const int ist
    	return a;
    } 
    
    int dec(int MUTABLE a)
    {
    	--a;
    	return a;
    }
    
    int sq(int a) //bei der typischen, korrekten Funktion ändert sich nichts
    {
    	return a * a;
    }
    
    #pragma pop_default_const_arguments
    


  • Swordfish schrieb:

    Außer, dass du weißt, das ich den Parameter bar in der Funktion nur lesen werde? Was ist daran phöse?

    Nope, diese Information hast du nicht. Ich kann den Wert in der Funktion wenn ich will auch beschreiben...

    Es hat zumindest einen Vorteil: Sollte ich aus versehen einen Parameter innerhalb der Funktion ändern wollen, macht mich durch const der Compiler darauf aufmerksam.

    Es handelt sich bei
    void foo(int const i);
    nur um die Deklaration der Funktion. Ob hier ein const steht oder nicht ist dem Compiler egal.

    In der Implementierung kann man das const stehen lassen oder wegnehmen, der Compiler kümmert sich nicht darum.

    Deshalb: das const in der Deklaration der Funktion ist schlecht: es ist unnötig und sagt nichts aus - was bei const immer böse ist, denn const soll ja auf etwas hinweisen. In diesem Fall aber ist das const zu ignorieren.

    In der Definition der Funktion kann man nun theoretisch das const setzen wenn man gerne unnötig Leute verwirrt. Das Problem dabei ist, dass man manchmal den Parameter aber doch ändern will, weil es für den Algorithmus halt gerade passt. Nun muss man die Signatur aber plötzlich anpassen. Und dann hat man komische Einträge in der Source Verwaltung in Zeilen die sich eigentlich nie ändern sollten...



  • camper schrieb:

    const void foo();
    void foo();
    

    sind unterschiedliche Funktionen (und können nat. auch nicht so überladen werden)

    100 Pluspunkte für "const void" als Rückgabetyp! 🙂
    Also, die Adressen solcher Funktionen kann ich jedenfalls mit dem GCC 4.7.2 nicht vergleichen, weil die Typen wie erwartet nicht übereinstimmen.

    Was mich aber gerade wundert ist, dass mir decltype des GCCs (4.7.2) sagt, dass das erste foo() void und nicht const als Return-Typ hat. Das ist glaub'ich ein Fehlverhalten des Compilers. Ich hätte folgendes erwartet:

    decltype(foo())   --> const void // sollte der Rückgabetyp der Funktion sein
    decltype((foo())) --> void       // sollte den Typ und die L/Rvalueness des Ausdrucks beschreiben
    

    (unter der Annahme, dass void sich auch wie ein skalarer Typ bzgl const verhält)

    Ich bekomme aber beide Male nur "void" zurück. 😕

    Was sagt der Experte?



  • ...


  • Mod

    krümelkacker schrieb:

    Was mich aber gerade wundert ist, dass mir decltype des GCCs (4.7.2) sagt, dass das erste foo() void und nicht const als Return-Typ hat. Das ist glaub'ich ein Fehlverhalten des Compilers. Ich hätte folgendes erwartet:

    decltype(foo())   --> const void // sollte der Rückgabetyp der Funktion sein
    decltype((foo())) --> void       // sollte den Typ und die L/Rvalueness des Ausdrucks beschreiben
    

    (unter der Annahme, dass void sich auch wie ein skalarer Typ bzgl const verhält)

    Ich bekomme aber beide Male nur "void" zurück. 😕

    Klammerung spielt bei decltype nur eine Rolle, wenn es der Ausdruck ohne Klammer ein id-Ausdruck oder ein Klassenmemberzugriff ist (hier nicht der Fall).

    n3337 schrieb:

    7.1.6.2/4 The type denoted by decltype(e) is defined as follows:
    — if e is an unparenthesized id-expression or an unparenthesized class member access (5.2.5), decltype(e)
    is the type of the entity named by e. If there is no such entity, or if e names a set of overloaded functions,
    the program is ill-formed;
    — otherwise, if e is an xvalue, decltype(e) is T&&, where T is the type of e;
    — otherwise, if e is an lvalue, decltype(e) is T&, where T is the type of e;
    — otherwise, decltype(e) is the type of e.

    foo() ist weder ein id-Ausdruck noch ein Klassenmemberzugriff. Ausserdem ist es ein prvalue, also ist die letzte Alternative anzuwenden.
    Und der Typ des Funktionsaufrufes ist void, da nicht-Klassen-prvalues stets unqualifiziert sind (3.10/4), gerade deswegen ist ja cv-Qualifizierung von Rückgabewerten in diesen Fällen so sinnlos.


Anmelden zum Antworten