Wie entferne ich Codeduplizierungen zwischen ähnlichen konstanten und nicht konstanten Elementfunktionen?

242

Angenommen, ich habe Folgendes, class Xwo ich den Zugriff auf ein internes Mitglied zurückgeben möchte:

class Z
{
    // details
};

class X
{
    std::vector<Z> vecZ;

public:
    Z& Z(size_t index)
    {
        // massive amounts of code for validating index

        Z& ret = vecZ[index];

        // even more code for determining that the Z instance
        // at index is *exactly* the right sort of Z (a process
        // which involves calculating leap years in which
        // religious holidays fall on Tuesdays for
        // the next thousand years or so)

        return ret;
    }
    const Z& Z(size_t index) const
    {
        // identical to non-const X::Z(), except printed in
        // a lighter shade of gray since
        // we're running low on toner by this point
    }
};

Die beiden Elemente funktionieren X::Z()und X::Z() consthaben identischen Code in geschweiften Klammern. Dies ist doppelter Code und kann Wartungsprobleme für lange Funktionen mit komplexer Logik verursachen .

Gibt es eine Möglichkeit, diese Codeduplizierung zu vermeiden?

Kevin
quelle
In diesem Beispiel würde ich im const-Fall einen Wert zurückgeben, damit Sie das unten stehende Refactoring nicht durchführen können. int Z () const {return z; }
Matt Price
1
Für grundlegende Typen sind Sie absolut richtig! Mein erstes Beispiel war nicht sehr gut. Nehmen wir an, wir geben stattdessen eine Klasseninstanz zurück. (Ich habe die Frage aktualisiert, um dies widerzuspiegeln.)
Kevin

Antworten:

189

Eine ausführliche Erläuterung finden Sie in der Überschrift "Vervielfältigung in constund Nichtmitgliedsfunktion vermeiden const" auf S. 32 . 23, in Punkt 3 " constWann immer möglich verwenden" in Effective C ++ , 3d, herausgegeben von Scott Meyers, ISBN-13: 9780321334879.

Alt-Text

Hier ist Meyers 'Lösung (vereinfacht):

struct C {
  const char & get() const {
    return c;
  }
  char & get() {
    return const_cast<char &>(static_cast<const C &>(*this).get());
  }
  char c;
};

Die beiden Besetzungen und der Funktionsaufruf mögen hässlich sein, aber sie sind korrekt. Meyers hat eine gründliche Erklärung warum.

jwfearn
quelle
45
Niemand wurde jemals entlassen, weil er Scott Meyers gefolgt war :-)
Steve Jessop
11
witkamp hat Recht, dass es im Allgemeinen schlecht ist, const_cast zu verwenden. Dies ist ein spezieller Fall, in dem dies nicht der Fall ist, wie Meyers erklärt. @Adam: ROM => const ist in Ordnung. const == ROM ist offensichtlich Unsinn, da jeder non-const wohl oder übel in const umwandeln kann: Es ist gleichbedeutend damit, einfach zu entscheiden, etwas nicht zu ändern.
Steve Jessop
44
Im Allgemeinen würde ich vorschlagen, const_cast anstelle von static_cast zu verwenden, um const hinzuzufügen, da dies verhindert, dass Sie den Typ versehentlich ändern.
Greg Rogers
6
@HelloGoodbye: Ich denke, Meyers geht vom Designer der Klassenschnittstelle von einem Minimum an Intelligenz aus. Wennget()const etwas zurückgegeben wird, das als const-Objekt definiert wurde, sollte es überhaupt keine Nicht-const-Version von geben get(). Tatsächlich hat sich mein Denken darüber im Laufe der Zeit geändert: Die Vorlagenlösung ist der einzige Weg, um Duplikate zu vermeiden und eine vom Compiler überprüfte Konstantenkorrektheit zu erhalten. Daher würde ich persönlich keine mehr verwenden const_cast, um das Duplizieren von Code zu vermeiden. Ich würde zwischen Putten wählen den betrogenen Code in eine Funktionsvorlage oder lassen ihn betrogen.
Steve Jessop
7
Die folgenden zwei Vorlagen tragen enorm zur Lesbarkeit dieser Lösung bei: template<typename T> const T& constant(T& _) { return const_cast<const T&>(_); }undtemplate<typename T> T& variable(const T& _) { return const_cast<T&>(_); } . Dann können Sie tun:return variable(constant(*this).get());
Casey Rodarmor
64

Ja, es ist möglich, die Codeduplizierung zu vermeiden. Sie müssen die const-Member-Funktion verwenden, um die Logik zu haben, und die non-const-Member-Funktion muss die const-Member-Funktion aufrufen und den Rückgabewert in eine nicht-const-Referenz umwandeln (oder einen Zeiger, wenn die Funktionen einen Zeiger zurückgeben):

class X
{
   std::vector<Z> vecZ;

public:
   const Z& z(size_t index) const
   {
      // same really-really-really long access 
      // and checking code as in OP
      // ...
      return vecZ[index];
   }

   Z& z(size_t index)
   {
      // One line. One ugly, ugly line - but just one line!
      return const_cast<Z&>( static_cast<const X&>(*this).z(index) );
   }

 #if 0 // A slightly less-ugly version
   Z& Z(size_t index)
   {
      // Two lines -- one cast. This is slightly less ugly but takes an extra line.
      const X& constMe = *this;
      return const_cast<Z&>( constMe.z(index) );
   }
 #endif
};

HINWEIS: Es ist wichtig, dass Sie die Logik NICHT in die Nicht-Konstanten-Funktion einfügen und die Konstanten-Funktion die Nicht-Konstanten-Funktion aufrufen lässt. Dies kann zu undefiniertem Verhalten führen. Der Grund ist, dass eine konstante Klasseninstanz als nicht konstante Instanz umgewandelt wird. Die Nicht-Const-Member-Funktion kann die Klasse versehentlich ändern, was nach den C ++ - Standardzuständen zu undefiniertem Verhalten führt.

Kevin
quelle
3
Wow ... das ist schrecklich. Sie haben gerade die Codemenge erhöht, die Klarheit verringert und zwei stinkende const_cast <> s hinzugefügt . Vielleicht haben Sie ein Beispiel im Sinn, wo dies tatsächlich Sinn macht?
Shog9
14
Hey, tu das nicht! Es mag hässlich sein, aber laut Scott Meyers ist es (fast) der richtige Weg. Siehe Effective C ++ , 3d ed, Punkt 3 unter der Überschrift "Vermeiden von Doppelarbeit in konstanten und nicht kostenpflichtigen Mitgliedsfunktionen.
jwfearn
17
Obwohl ich verstehe, dass die Lösung hässlich sein kann, stellen Sie sich vor, dass der Code, der bestimmt, was zurückgegeben werden soll, 50 Zeilen lang ist. Dann ist eine Duplizierung höchst unerwünscht - insbesondere, wenn Sie den Code neu faktorisieren müssen. Das habe ich in meiner Karriere schon oft erlebt.
Kevin
8
Der Unterschied zwischen diesem und Meyers besteht darin, dass Meyers static_cast <const X &> (* this) hat. const_cast dient zum Entfernen von const und nicht zum Hinzufügen.
Steve Jessop
8
@VioletGiraffe Wir wissen, dass das Objekt ursprünglich nicht als Konstante erstellt wurde, da es ein Nicht-Konstanten-Mitglied eines Nicht-Konstanten-Objekts ist, was wir wissen, weil wir uns in einer Nicht-Konstanten-Methode des Objekts befinden. Der Compiler macht diese Schlussfolgerung nicht, sondern folgt einer konservativen Regel. Warum gibt es Ihrer Meinung nach const_cast, wenn nicht für diese Art von Situation?
Caleth
47

C ++ 17 hat die beste Antwort auf diese Frage aktualisiert:

T const & f() const {
    return something_complicated();
}
T & f() {
    return const_cast<T &>(std::as_const(*this).f());
}

Dies hat die Vorteile, dass es:

  • Ist offensichtlich, was los ist
  • Hat minimalen Code-Overhead - es passt in eine einzelne Zeile
  • Ist schwer falsch zu verstehen (kann nur versehentlich weggeworfen volatilewerden, ist aber volatileein seltenes Qualifikationsspiel)

Wenn Sie den vollständigen Abzugsweg gehen möchten, können Sie dies durch eine Hilfsfunktion erreichen

template<typename T>
constexpr T & as_mutable(T const & value) noexcept {
    return const_cast<T &>(value);
}
template<typename T>
constexpr T * as_mutable(T const * value) noexcept {
    return const_cast<T *>(value);
}
template<typename T>
constexpr T * as_mutable(T * value) noexcept {
    return value;
}
template<typename T>
void as_mutable(T const &&) = delete;

Jetzt können Sie nicht einmal durcheinander bringen volatile, und die Nutzung sieht so aus

decltype(auto) f() const {
    return something_complicated();
}
decltype(auto) f() {
    return as_mutable(std::as_const(*this).f());
}
David Stone
quelle
Beachten Sie, dass „as_mutable“ mit dem const rvalue Überlastungs Gelöscht (was im allgemeinen vorzuziehen) verhindert , dass das letzte Beispiel von der Arbeit , wenn f()zurückkehrt , Tstatt T&.
Max Truxa
1
@ MaxTruxa: Ja, und das ist eine gute Sache. Wenn es nur kompiliert würde, hätten wir eine baumelnde Referenz. Für den Fall, dass f()Retouren Tnicht zwei Überladungen haben sollen, ist die constVersion allein ausreichend.
David Stone
Sehr wahr, ich entschuldige mich für meinen vollen Hirnfurz gestern, keine Ahnung, woran ich dachte, als ich diesen Kommentar schrieb. Ich habe mir ein const / veränderliches Getter-Paar angesehen, das a zurückgibt shared_ptr. Was ich also wirklich brauchte, war so etwas, as_mutable_ptrdas fast identisch mit dem as_mutableoben genannten aussieht , außer dass es a nimmt und zurückgibt shared_ptrund std::const_pointer_caststattdessen verwendet const_cast.
Max Truxa
1
Wenn eine Methode zurückkehrt, T const*wird diese T const* const&&eher gebunden als gebunden T const* const&(zumindest in meinen Tests). Ich musste eine Überladung T const*als Argumenttyp für Methoden hinzufügen , die einen Zeiger zurückgeben.
monkey0506
2
@ monkey0506: Ich habe meine Antwort aktualisiert, um Zeiger und Referenzen zu unterstützen
David Stone
34

Ich denke, die Lösung von Scott Meyers kann in C ++ 11 durch die Verwendung einer temporären Hilfsfunktion verbessert werden. Dies macht die Absicht viel offensichtlicher und kann für viele andere Getter wiederverwendet werden.

template <typename T>
struct NonConst {typedef T type;};
template <typename T>
struct NonConst<T const> {typedef T type;}; //by value
template <typename T>
struct NonConst<T const&> {typedef T& type;}; //by reference
template <typename T>
struct NonConst<T const*> {typedef T* type;}; //by pointer
template <typename T>
struct NonConst<T const&&> {typedef T&& type;}; //by rvalue-reference

template<typename TConstReturn, class TObj, typename... TArgs>
typename NonConst<TConstReturn>::type likeConstVersion(
   TObj const* obj,
   TConstReturn (TObj::* memFun)(TArgs...) const,
   TArgs&&... args) {
      return const_cast<typename NonConst<TConstReturn>::type>(
         (obj->*memFun)(std::forward<TArgs>(args)...));
}

Diese Hilfsfunktion kann folgendermaßen verwendet werden.

struct T {
   int arr[100];

   int const& getElement(size_t i) const{
      return arr[i];
   }

   int& getElement(size_t i) {
      return likeConstVersion(this, &T::getElement, i);
   }
};

Das erste Argument ist immer der this-Zeiger. Der zweite ist der Zeiger auf die aufzurufende Elementfunktion. Danach kann eine beliebige Anzahl zusätzlicher Argumente übergeben werden, damit diese an die Funktion weitergeleitet werden können. Dies erfordert C ++ 11 aufgrund der verschiedenen Vorlagen.

Pait
quelle
3
Es ist eine Schande, dass wir nicht std::remove_bottom_constmitgehen müssen std::remove_const.
TBBle
Ich mag diese Lösung nicht, weil sie immer noch a einbettet const_cast. Sie können selbst getElementeine Vorlage erstellen und das Merkmal des darin enthaltenen Typs für die benötigten mpl::conditionalTypen verwenden, z. B. iterators oder constiterators, falls erforderlich. Das eigentliche Problem ist, wie eine const-Version einer Methode generiert wird, wenn dieser Teil der Signatur nicht als Vorlage verwendet werden kann.
v.oddou
2
@ v.oddou: std::remove_const<int const&>ist int const &(Top-Level- constQualifikation entfernen ), daher die Gymnastik NonConst<T>in dieser Antwort. Putative std::remove_bottom_constkönnte die constQualifikation der untersten Ebene entfernen und genau das tun, was NonConst<T>hier geschieht: std::remove_bottom_const<int const&>::type=> int&.
TBBle
4
Diese Lösung funktioniert nicht gut, wenn sie getElementüberlastet ist. Dann kann der Funktionszeiger nicht aufgelöst werden, ohne die Vorlagenparameter explizit anzugeben. Warum?
John
1
Sie müssen Ihre Antwort korrigieren, um die perfekte Weiterleitung von C ++ 11 zu verwenden: likeConstVersion(TObj const* obj, TConstReturn (TObj::*memFun)(TArgs...) const, TArgs&&... args) { return const_cast<typename NonConst<TConstReturn>::type>((obj->*memFun)(std::forward<TArgs>(args)...)); }Vollständig: gist.github.com/BlueSolei/bca26a8590265492e2f2760d3cefcf83
ShaulF
22

Ein bisschen ausführlicher als Meyers, aber ich könnte das tun:

class X {

    private:

    // This method MUST NOT be called except from boilerplate accessors.
    Z &_getZ(size_t index) const {
        return something;
    }

    // boilerplate accessors
    public:
    Z &getZ(size_t index)             { return _getZ(index); }
    const Z &getZ(size_t index) const { return _getZ(index); }
};

Die private Methode hat die unerwünschte Eigenschaft, dass sie ein nicht-const Z & für eine const-Instanz zurückgibt, weshalb sie privat ist. Private Methoden können Invarianten der externen Schnittstelle aufbrechen (in diesem Fall ist die gewünschte Invariante "ein const-Objekt kann nicht über Verweise auf Objekte geändert werden, die es hat-a").

Beachten Sie, dass die Kommentare Teil des Musters sind - die Benutzeroberfläche von _getZ gibt an, dass es niemals gültig ist, sie aufzurufen (abgesehen von den Accessoren natürlich): Es ist ohnehin kein denkbarer Vorteil, dies zu tun, da 1 Zeichen mehr eingegeben werden muss und nicht führen zu kleinerem oder schnellerem Code. Das Aufrufen der Methode entspricht dem Aufrufen eines der Accessoren mit einem const_cast, und das möchten Sie auch nicht. Wenn Sie sich Sorgen machen, Fehler offensichtlich zu machen (und das ist ein faires Ziel), nennen Sie es const_cast_getZ anstelle von _getZ.

Ich schätze übrigens Meyers Lösung. Ich habe keine philosophischen Einwände dagegen. Persönlich bevorzuge ich jedoch ein kleines Stück kontrollierter Wiederholung und eine private Methode, die nur unter bestimmten streng kontrollierten Umständen aufgerufen werden darf, gegenüber einer Methode, die wie Linienrauschen aussieht. Wählen Sie Ihr Gift und bleiben Sie dabei.

[Bearbeiten: Kevin hat zu Recht darauf hingewiesen, dass _getZ möglicherweise eine weitere Methode (z. B. generateZ) aufrufen möchte, die auf die gleiche Weise wie getZ auf const spezialisiert ist. In diesem Fall würde _getZ ein const Z & sehen und müsste es vor der Rückkehr const_cast. Das ist immer noch sicher, da der Boilerplate-Zubehör alles überwacht, aber es ist nicht besonders offensichtlich, dass es sicher ist. Wenn Sie dies tun und später generateZ ändern, um immer const zurückzugeben, müssen Sie außerdem getZ ändern, um immer const zurückzugeben, aber der Compiler sagt Ihnen nicht, dass Sie dies tun.

Dieser letztere Punkt über den Compiler gilt auch für Meyers empfohlenes Muster, der erste Punkt über einen nicht offensichtlichen const_cast jedoch nicht. Alles in allem denke ich, dass, wenn sich herausstellt, dass _getZ einen const_cast für seinen Rückgabewert benötigt, dieses Muster einen großen Teil seines Wertes gegenüber Meyers verliert. Da es auch Nachteile gegenüber Meyers hat, denke ich, dass ich in dieser Situation zu seinem wechseln würde. Das Refactoring von einem zum anderen ist einfach - es wirkt sich nicht auf anderen gültigen Code in der Klasse aus, da nur ungültiger Code und das Boilerplate _getZ aufrufen.]

Steve Jessop
quelle
3
Dies hat immer noch das Problem, dass das, was Sie zurückgeben, für eine konstante Instanz von X konstant sein kann. In diesem Fall benötigen Sie immer noch einen const_cast in _getZ (...). Wenn es von späteren Entwicklern missbraucht wird, kann es dennoch zu UB führen. Wenn das zurückgegebene Objekt "veränderlich" ist, ist dies eine gute Lösung.
Kevin
1
Jede private Funktion (zum Teufel auch öffentliche) kann von späteren Entwicklern missbraucht werden, wenn sie die BLOCK CAPITAL-Anweisungen zu ihrer gültigen Verwendung, in der Header-Datei und auch in Doxygen usw. ignorieren. Ich kann das nicht aufhalten. und ich betrachte es nicht als mein Problem, da die Anweisungen leicht zu verstehen sind.
Steve Jessop
13
-1: Dies funktioniert in vielen Situationen nicht. Was ist, wenn somethingin der _getZ()Funktion eine Instanzvariable enthalten ist? Der Compiler (oder zumindest einige Compiler) wird sich darüber beschweren, dass da _getZ()const ist, auch jede Instanzvariable, auf die darin verwiesen wird, const ist. Also somethingwäre dann const (es wäre vom Typ const Z&) und könnte nicht konvertiert werden Z&. Nach meiner (zugegebenermaßen etwas begrenzten) Erfahrung ist die meiste Zeit somethingin solchen Fällen eine Instanzvariable.
Schwerkraft
2
@GravityBringer: dann muss "etwas" a beinhalten const_cast. Es war beabsichtigt , ein Platzhalter zu sein , dass der Code eine nicht-const Rückkehr aus dem const - Objekt erhalten erforderlich, nicht als Platzhalter für das, was würde in dem duplizierten Getter gewesen. "Etwas" ist also nicht nur eine Instanzvariable.
Steve Jessop
2
Aha. Das verringert jedoch wirklich den Nutzen der Technik. Ich würde die Ablehnung entfernen, aber SO lässt mich nicht.
Schwerkraft
22

Schöne Frage und nette Antworten. Ich habe eine andere Lösung, die keine Abgüsse verwendet:

class X {

private:

    std::vector<Z> v;

    template<typename InstanceType>
    static auto get(InstanceType& instance, std::size_t i) -> decltype(instance.get(i)) {
        // massive amounts of code for validating index
        // the instance variable has to be used to access class members
        return instance.v[i];
    }

public:

    const Z& get(std::size_t i) const {
        return get(*this, i);
    }

    Z& get(std::size_t i) {
        return get(*this, i);
    }

};

Es hat jedoch die Hässlichkeit, ein statisches Element zu benötigen, und die Notwendigkeit, das zu verwenden instance Variable zu verwenden.

Ich habe nicht alle möglichen (negativen) Auswirkungen dieser Lösung berücksichtigt. Bitte lassen Sie mich wissen, wenn überhaupt.

gd1
quelle
4
Nun, lassen Sie uns mit der einfachen Tatsache fortfahren, dass Sie mehr Boilerplate hinzugefügt haben. Wenn überhaupt, sollte dies als Beispiel dafür dienen, warum die Sprache eine Möglichkeit benötigt, die Funktionsqualifizierer zusammen mit dem Rückgabetyp zu ändern auto get(std::size_t i) -> auto(const), auto(&&). Warum '&&'? Ahh, also kann ich sagen:auto foo() -> auto(const), auto(&&) = delete;
kfsone
gd1: genau das, was ich mir vorgestellt habe. @kfsone und genau das, was ich auch geschlossen habe.
v.oddou
1
@kfsone Die Syntax sollte ein thisSchlüsselwort enthalten. Ich schlage vor, dass template< typename T > auto myfunction(T this, t args) -> decltype(ident)dieses Schlüsselwort als implizites Objektinstanzargument erkannt wird und der Compiler erkennt, dass myfunction ein Mitglied ist oder T. Twird automatisch auf der Anrufseite abgeleitet, die immer der Typ der Klasse ist, jedoch mit kostenloser Lebenslaufqualifikation.
v.oddou
2
Diese Lösung hat auch den Vorteil (gegenüber der const_casteinen), dass sie zurückkehren kann iteratorund const_iterator.
Jarod42
1
Wenn die Implementierung in eine CPP-Datei verschoben wird (und da die Methode zum Nicht-Duplizieren nicht trivial sein sollte, ist dies wahrscheinlich der Fall), statickann dies im Dateibereich anstelle des Klassenbereichs erfolgen. :-)
Jarod42
8

Sie können dies auch mit Vorlagen lösen. Diese Lösung ist etwas hässlich (aber die Hässlichkeit ist in der CPP-Datei verborgen), bietet jedoch eine Compiler-Überprüfung der Konstanz und keine Codeduplizierung.

.h Datei:

#include <vector>

class Z
{
    // details
};

class X
{
    std::vector<Z> vecZ;

public:
    const std::vector<Z>& GetVector() const { return vecZ; }
    std::vector<Z>& GetVector() { return vecZ; }

    Z& GetZ( size_t index );
    const Z& GetZ( size_t index ) const;
};

CPP-Datei:

#include "constnonconst.h"

template< class ParentPtr, class Child >
Child& GetZImpl( ParentPtr parent, size_t index )
{
    // ... massive amounts of code ...

    // Note you may only use methods of X here that are
    // available in both const and non-const varieties.

    Child& ret = parent->GetVector()[index];

    // ... even more code ...

    return ret;
}

Z& X::GetZ( size_t index )
{
    return GetZImpl< X*, Z >( this, index );
}

const Z& X::GetZ( size_t index ) const
{
    return GetZImpl< const X*, const Z >( this, index );
}

Der Hauptnachteil, den ich sehen kann, ist, dass Sie, da sich die gesamte komplexe Implementierung der Methode in einer globalen Funktion befindet, entweder die Mitglieder von X mit öffentlichen Methoden wie GetVector () oben erreichen müssen (von denen es immer eine geben muss) const und non-const version) oder Sie könnten diese Funktion zu einem Freund machen. Aber ich mag keine Freunde.

[Bearbeiten: Nicht benötigtes Include von cstdio entfernt, das während des Tests hinzugefügt wurde.]

Andy Bileam
quelle
3
Sie können die komplexe Implementierungsfunktion jederzeit zu einem statischen Element machen, um Zugriff auf die privaten Mitglieder zu erhalten. Die Funktion muss nur in der Klassenheaderdatei deklariert werden, die Definition kann sich in der Klassenimplementierungsdatei befinden. Es ist schließlich Teil der Klassenimplementierung.
CB Bailey
Aah ja gute Idee! Ich mag das Vorlagenmaterial, das in der Kopfzeile angezeigt wird, nicht, aber wenn es die Implementierung möglicherweise viel einfacher macht, lohnt es sich wahrscheinlich.
Andy Bileam
+ 1 für diese Lösung, die keinen Code dupliziert oder hässlich verwendet const_cast(was versehentlich verwendet werden könnte, um etwas zu verhindern, von dem eigentlich angenommen wird, dass es sich um etwas handelt, das nicht konstant ist).
HelloGoodbye
Heutzutage kann dies durch einen abgeleiteten Rückgabetyp für die Vorlage vereinfacht werden (besonders nützlich, da dadurch reduziert wird, was im Mitgliedsfall in der Klasse dupliziert werden muss).
Davis Herring
3

Wie wäre es, wenn Sie die Logik in eine private Methode verschieben und nur das Zeug "Referenz abrufen und zurückgeben" in den Gettern ausführen? Eigentlich wäre ich ziemlich verwirrt über die statischen und konstanten Casts in einer einfachen Getter-Funktion, und ich würde das bis auf äußerst seltene Umstände für hässlich halten!

MP24
quelle
Um undefiniertes Verhalten zu vermeiden, benötigen Sie noch einen const_cast. Siehe die Antwort von Martin York und meinen Kommentar dort.
Kevin
1
Kevin, welche Antwort von Martin York
Peter Nimmo
2

Betrügt es, den Präprozessor zu benutzen?

struct A {

    #define GETTER_CORE_CODE       \
    /* line 1 of getter code */    \
    /* line 2 of getter code */    \
    /* .....etc............. */    \
    /* line n of getter code */       

    // ^ NOTE: line continuation char '\' on all lines but the last

   B& get() {
        GETTER_CORE_CODE
   }

   const B& get() const {
        GETTER_CORE_CODE
   }

   #undef GETTER_CORE_CODE

};

Es ist nicht so ausgefallen wie Vorlagen oder Casts, aber es macht Ihre Absicht ("diese beiden Funktionen sollen identisch sein") ziemlich explizit.

user1476176
quelle
1
Aber dann müssen Sie vorsichtig mit Backslashes sein (wie bei mehrzeiligen Makros üblich) und außerdem verlieren Sie in den meisten (wenn nicht allen) Editoren die Syntaxhervorhebung.
Ruslan
2

Es ist für mich überraschend, dass es so viele verschiedene Antworten gibt, aber fast alle auf schwere Vorlagenmagie angewiesen sind. Vorlagen sind leistungsstark, aber manchmal schlagen Makros sie kurz und bündig. Maximale Vielseitigkeit wird oft durch die Kombination beider erreicht.

Ich habe ein Makro geschrieben FROM_CONST_OVERLOAD() das in die Nicht-Konstanten-Funktion eingefügt werden kann, um die Konstanten-Funktion aufzurufen.

Anwendungsbeispiel:

class MyClass
{
private:
    std::vector<std::string> data = {"str", "x"};

public:
    // Works for references
    const std::string& GetRef(std::size_t index) const
    {
        return data[index];
    }

    std::string& GetRef(std::size_t index)
    {
        return FROM_CONST_OVERLOAD( GetRef(index) );
    }


    // Works for pointers
    const std::string* GetPtr(std::size_t index) const
    {
        return &data[index];
    }

    std::string* GetPtr(std::size_t index)
    {
        return FROM_CONST_OVERLOAD( GetPtr(index) );
    }
};

Einfache und wiederverwendbare Implementierung:

template <typename T>
T& WithoutConst(const T& ref)
{
    return const_cast<T&>(ref);
}

template <typename T>
T* WithoutConst(const T* ptr)
{
    return const_cast<T*>(ptr);
}

template <typename T>
const T* WithConst(T* ptr)
{
    return ptr;
}

#define FROM_CONST_OVERLOAD(FunctionCall) \
  WithoutConst(WithConst(this)->FunctionCall)

Erläuterung:

Wie in vielen Antworten angegeben, lautet das typische Muster zur Vermeidung von Codeduplizierungen in einer Nicht-Const-Member-Funktion wie folgt:

return const_cast<Result&>( static_cast<const MyClass*>(this)->Method(args) );

Ein Großteil dieser Kesselplatte kann durch Typinferenz vermieden werden. Erstens const_castkann eingekapselt werden WithoutConst(), wodurch der Typ des Arguments abgeleitet und das const-Qualifikationsmerkmal entfernt wird. Zweitens kann ein ähnlicher Ansatz verwendet werden, um die WithConst()zu qualifizierenthis Zeiger , wodurch der Aufruf der Methode const-overloaded ermöglicht wird.

Der Rest ist ein einfaches Makro, das dem Aufruf das korrekt qualifizierte Präfix voranstellt this->und const aus dem Ergebnis entfernt. Da der im Makro verwendete Ausdruck fast immer ein einfacher Funktionsaufruf mit 1: 1-Weiterleitungsargumenten ist, treten Nachteile von Makros wie Mehrfachauswertung nicht auf. Die Auslassungspunkte und__VA_ARGS__ könnten auch verwendet werden, sollten aber nicht benötigt werden, da Kommas (as Argumenttrennzeichen) stehen in Klammern.

Dieser Ansatz hat mehrere Vorteile:

  • Minimale und natürliche Syntax - schließen Sie den Anruf einfach ein FROM_CONST_OVERLOAD( )
  • Keine zusätzliche Mitgliedsfunktion erforderlich
  • Kompatibel mit C ++ 98
  • Einfache Implementierung, keine Template-Metaprogrammierung und keine Abhängigkeiten
  • Extensible: andere const Beziehungen können hinzugefügt werden (wie const_iterator, std::shared_ptr<const T>usw.). Überladen Sie dazu einfach WithoutConst()die entsprechenden Typen.

Einschränkungen: Diese Lösung ist für Szenarien optimiert, in denen die Nicht-Konstanten-Überladung genau das Gleiche tut wie die Konstanten-Überladung, sodass Argumente 1: 1 weitergeleitet werden können. Wenn sich Ihre Logik unterscheidet und Sie die const-Version nicht über aufrufen this->Method(args), können Sie andere Ansätze in Betracht ziehen.

TheOperator
quelle
2

Für diejenigen (wie ich), die

  • benutze c ++ 17
  • möchte die geringste Menge an Boilerplate / Wiederholung hinzufügen und
  • Es macht nichts aus, Makros zu verwenden (während Sie auf Metaklassen warten ...),

Hier ist eine andere Einstellung:

#include <utility>
#include <type_traits>

template <typename T> struct NonConst;
template <typename T> struct NonConst<T const&> {using type = T&;};
template <typename T> struct NonConst<T const*> {using type = T*;};

#define NON_CONST(func)                                                     \
    template <typename... T> auto func(T&&... a)                            \
        -> typename NonConst<decltype(func(std::forward<T>(a)...))>::type   \
    {                                                                       \
        return const_cast<decltype(func(std::forward<T>(a)...))>(           \
            std::as_const(*this).func(std::forward<T>(a)...));              \
    }

Es ist im Grunde eine Mischung aus den Antworten von @Pait, @DavidStone und @ sh1 ( EDIT : und eine Verbesserung von @cdhowie). Was der Tabelle hinzugefügt wird, ist, dass Sie nur eine zusätzliche Codezeile erhalten, die einfach die Funktion benennt (aber keine Argument- oder Rückgabetyp-Duplizierung):

class X
{
    const Z& get(size_t index) const { ... }
    NON_CONST(get)
};

Hinweis: gcc kann dies nicht vor 8.1 kompilieren, clang-5 und höher sowie MSVC-19 sind zufrieden (laut Compiler-Explorer ).

Axxel
quelle
Das hat bei mir einfach geklappt. Dies ist eine großartige Antwort, danke!
Kurz
Sollten die decltype()s nicht auch std::forwarddie Argumente verwenden, um sicherzustellen, dass wir den richtigen Rückgabetyp verwenden, wenn wir Überladungen davon haben get(), die unterschiedliche Arten von Referenzen annehmen?
cdhowie
@cdhowie Können Sie ein Beispiel geben?
Axxel
@axxel Es ist höllisch erfunden, aber los geht's . Das NON_CONSTMakro leitet den Rückgabetyp falsch ab und const_casts auf den falschen Typ, da die decltype(func(a...))Typen nicht weitergeleitet werden. Das Ersetzen durch decltype(func(std::forward<T>(a)...)) Löst dies . (Es gibt nur einen Linker-Fehler, da ich keine der deklarierten X::getÜberladungen definiert habe .)
cdhowie
1
Vielen Dank an @cdhowie, ich habe Ihr Beispiel aufgepimpt, um die nicht konstanten
axxel
1

Hier ist eine C ++ 17-Version der statischen Hilfsfunktion der Vorlage mit und optionalem SFINAE-Test.

#include <type_traits>

#define REQUIRES(...)         class = std::enable_if_t<(__VA_ARGS__)>
#define REQUIRES_CV_OF(A,B)   REQUIRES( std::is_same_v< std::remove_cv_t< A >, B > )

class Foobar {
private:
    int something;

    template<class FOOBAR, REQUIRES_CV_OF(FOOBAR, Foobar)>
    static auto& _getSomething(FOOBAR& self, int index) {
        // big, non-trivial chunk of code...
        return self.something;
    }

public:
    auto& getSomething(int index)       { return _getSomething(*this, index); }
    auto& getSomething(int index) const { return _getSomething(*this, index); }
};

Vollversion: https://godbolt.org/z/mMK4r3

atablash
quelle
1

Ich habe mir ein Makro ausgedacht, das automatisch Paare von const / non-const-Funktionen generiert.

class A
{
    int x;    
  public:
    MAYBE_CONST(
        CV int &GetX() CV {return x;}
        CV int &GetY() CV {return y;}
    )

    //   Equivalent to:
    // int &GetX() {return x;}
    // int &GetY() {return y;}
    // const int &GetX() const {return x;}
    // const int &GetY() const {return y;}
};

Siehe das Ende der Antwort für die Implementierung.

Das Argument von MAYBE_CONSTwird dupliziert. In der ersten Kopie CVwird durch nichts ersetzt; und in der zweiten Kopie wird es durch ersetztconst .

Es gibt keine Begrenzung, wie oft CV im Makroargument erscheinen kann.

Es gibt jedoch eine leichte Unannehmlichkeit. Wenn CVin Klammern angezeigt wird, muss diesem Klammerpaar Folgendes vorangestellt werden CV_IN:

// Doesn't work
MAYBE_CONST( CV int &foo(CV int &); )

// Works, expands to
//         int &foo(      int &);
//   const int &foo(const int &);
MAYBE_CONST( CV int &foo CV_IN(CV int &); )

Implementierung:

#define MAYBE_CONST(...) IMPL_CV_maybe_const( (IMPL_CV_null,__VA_ARGS__)() )
#define CV )(IMPL_CV_identity,
#define CV_IN(...) )(IMPL_CV_p_open,)(IMPL_CV_null,__VA_ARGS__)(IMPL_CV_p_close,)(IMPL_CV_null,

#define IMPL_CV_null(...)
#define IMPL_CV_identity(...) __VA_ARGS__
#define IMPL_CV_p_open(...) (
#define IMPL_CV_p_close(...) )

#define IMPL_CV_maybe_const(seq) IMPL_CV_a seq IMPL_CV_const_a seq

#define IMPL_CV_body(cv, m, ...) m(cv) __VA_ARGS__

#define IMPL_CV_a(...) __VA_OPT__(IMPL_CV_body(,__VA_ARGS__) IMPL_CV_b)
#define IMPL_CV_b(...) __VA_OPT__(IMPL_CV_body(,__VA_ARGS__) IMPL_CV_a)

#define IMPL_CV_const_a(...) __VA_OPT__(IMPL_CV_body(const,__VA_ARGS__) IMPL_CV_const_b)
#define IMPL_CV_const_b(...) __VA_OPT__(IMPL_CV_body(const,__VA_ARGS__) IMPL_CV_const_a)

Pre-C ++ 20-Implementierung, die nicht unterstützt CV_IN:

#define MAYBE_CONST(...) IMPL_MC( ((__VA_ARGS__)) )
#define CV ))((

#define IMPL_MC(seq) \
    IMPL_MC_end(IMPL_MC_a seq) \
    IMPL_MC_end(IMPL_MC_const_0 seq)

#define IMPL_MC_identity(...) __VA_ARGS__
#define IMPL_MC_end(...) IMPL_MC_end_(__VA_ARGS__)
#define IMPL_MC_end_(...) __VA_ARGS__##_end

#define IMPL_MC_a(elem) IMPL_MC_identity elem IMPL_MC_b
#define IMPL_MC_b(elem) IMPL_MC_identity elem IMPL_MC_a
#define IMPL_MC_a_end
#define IMPL_MC_b_end

#define IMPL_MC_const_0(elem)       IMPL_MC_identity elem IMPL_MC_const_a
#define IMPL_MC_const_a(elem) const IMPL_MC_identity elem IMPL_MC_const_b
#define IMPL_MC_const_b(elem) const IMPL_MC_identity elem IMPL_MC_const_a
#define IMPL_MC_const_a_end
#define IMPL_MC_const_b_end
HolyBlackCat
quelle
0

In der Regel sind die Elementfunktionen, für die Sie konstante und nicht konstante Versionen benötigen, Getter und Setter. Meistens handelt es sich um Einzeiler, sodass die Codeduplizierung kein Problem darstellt.

Dima
quelle
2
Das mag die meiste Zeit wahr sein. Es gibt jedoch Ausnahmen.
Kevin
1
Getter sowieso, ein Const Setter macht nicht viel Sinn;)
Jwfearn
Ich meinte, dass der Nicht-Konstanten-Getter effektiv ein Setter ist. :)
Dima
0

Ich habe dies für einen Freund const_castgetan, der die Verwendung von ... zu Recht gerechtfertigt hat. Ohne es zu wissen, hätte ich wahrscheinlich so etwas getan (nicht wirklich elegant):

#include <iostream>

class MyClass
{

public:

    int getI()
    {
        std::cout << "non-const getter" << std::endl;
        return privateGetI<MyClass, int>(*this);
    }

    const int getI() const
    {
        std::cout << "const getter" << std::endl;
        return privateGetI<const MyClass, const int>(*this);
    }

private:

    template <class C, typename T>
    static T privateGetI(C c)
    {
        //do my stuff
        return c._i;
    }

    int _i;
};

int main()
{
    const MyClass myConstClass = MyClass();
    myConstClass.getI();

    MyClass myNonConstClass;
    myNonConstClass.getI();

    return 0;
}
Matovitch
quelle
0

Ich würde eine statische Funktionsvorlage für private Helfer wie folgt vorschlagen:

class X
{
    std::vector<Z> vecZ;

    // ReturnType is explicitly 'Z&' or 'const Z&'
    // ThisType is deduced to be 'X' or 'const X'
    template <typename ReturnType, typename ThisType>
    static ReturnType Z_impl(ThisType& self, size_t index)
    {
        // massive amounts of code for validating index
        ReturnType ret = self.vecZ[index];
        // even more code for determining, blah, blah...
        return ret;
    }

public:
    Z& Z(size_t index)
    {
        return Z_impl<Z&>(*this, index);
    }
    const Z& Z(size_t index) const
    {
        return Z_impl<const Z&>(*this, index);
    }
};
Dats
quelle
-1

Dieser DDJ-Artikel zeigt eine Möglichkeit zur Verwendung der Vorlagenspezialisierung, bei der Sie const_cast nicht verwenden müssen. Für eine so einfache Funktion wird es allerdings wirklich nicht benötigt.

boost :: any_cast (an einem Punkt nicht mehr) verwendet einen const_cast aus der const-Version, der die Nicht-const-Version aufruft, um Doppelungen zu vermeiden. Sie können der Nicht-Const-Version jedoch keine konstante Semantik auferlegen, daher müssen Sie sehr vorsichtig sein vorsichtig sein.

Am Ende ist eine gewisse Codeduplizierung in Ordnung, solange sich die beiden Snippets direkt übereinander befinden.

Greg Rogers
quelle
Der DDJ-Artikel scheint sich auf Iteratoren zu beziehen - was für die Frage nicht relevant ist. Konst-Iteratoren sind keine konstanten Daten - sie sind Iteratoren, die auf konstante Daten verweisen.
Kevin
-1

Um die bereitgestellte Lösung jwfearn und kevin zu ergänzen, ist hier die entsprechende Lösung, wenn die Funktion shared_ptr zurückgibt:

struct C {
  shared_ptr<const char> get() const {
    return c;
  }
  shared_ptr<char> get() {
    return const_pointer_cast<char>(static_cast<const C &>(*this).get());
  }
  shared_ptr<char> c;
};
Christer Swahn
quelle
-1

Ich habe nicht gefunden, wonach ich gesucht habe, also habe ich ein paar meiner eigenen gerollt ...

Dieser ist ein wenig wortreich, hat aber den Vorteil, dass viele überladene Methoden mit demselben Namen (und Rückgabetyp) gleichzeitig behandelt werden:

struct C {
  int x[10];

  int const* getp() const { return x; }
  int const* getp(int i) const { return &x[i]; }
  int const* getp(int* p) const { return &x[*p]; }

  int const& getr() const { return x[0]; }
  int const& getr(int i) const { return x[i]; }
  int const& getr(int* p) const { return x[*p]; }

  template<typename... Ts>
  auto* getp(Ts... args) {
    auto const* p = this;
    return const_cast<int*>(p->getp(args...));
  }

  template<typename... Ts>
  auto& getr(Ts... args) {
    auto const* p = this;
    return const_cast<int&>(p->getr(args...));
  }
};

Wenn Sie nur eine constMethode pro Name haben, aber dennoch viele Methoden zum Duplizieren vorhanden sind, bevorzugen Sie möglicherweise Folgendes:

  template<typename T, typename... Ts>
  auto* pwrap(T const* (C::*f)(Ts...) const, Ts... args) {
    return const_cast<T*>((this->*f)(args...));
  }

  int* getp_i(int i) { return pwrap(&C::getp_i, i); }
  int* getp_p(int* p) { return pwrap(&C::getp_p, p); }

Leider bricht dies zusammen, sobald Sie anfangen, den Namen zu überladen (die Argumentliste des Funktionszeigerarguments scheint zu diesem Zeitpunkt ungelöst zu sein, sodass keine Übereinstimmung für das Funktionsargument gefunden werden kann). Obwohl Sie auch einen Ausweg daraus finden können:

  template<typename... Ts>
  auto* getp(Ts... args) { return pwrap<int, Ts...>(&C::getp, args...); }

Referenzargumente für die constMethode stimmen jedoch nicht mit den scheinbar by-value-Argumenten für die Vorlage überein, und sie wird unterbrochen. Nicht sicher warum. Hier ist warum .

sh1
quelle