Comment supprimer la duplication de code entre des fonctions membres const et non const similaires?

243

Supposons que je dispose des éléments suivants class Xoù je souhaite retourner l'accès à un membre interne:

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
    }
};

Les deux fonctions membres X::Z()et X::Z() constont un code identique à l'intérieur des accolades. Il s'agit de code en double et peut entraîner des problèmes de maintenance pour les fonctions longues avec une logique complexe .

Existe-t-il un moyen d'éviter cette duplication de code?

Kevin
la source
Dans cet exemple, je retournerais une valeur dans le cas const afin que vous ne puissiez pas le refactoring ci-dessous. int Z () const {return z; }
Matt Price
1
Pour les types fondamentaux, vous avez absolument raison! Mon premier exemple n'était pas très bon. Disons qu'à la place, nous retournons une instance de classe à la place. (J'ai mis à jour la question pour refléter cela.)
Kevin

Réponses:

190

Pour une explication détaillée, veuillez consulter la rubrique «Éviter la duplication dans les fonctions constnon constmembres», à la p. 23, dans le point 3 «Utiliser constautant que possible», dans Effective C ++ , 3ème éd par Scott Meyers, ISBN-13: 9780321334879.

texte alternatif

Voici la solution de Meyers (simplifiée):

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

Les deux transtypages et l'appel de fonction peuvent être laids mais c'est correct. Meyers a une explication approfondie pourquoi.

jwfearn
la source
45
Personne n'a jamais été licencié pour avoir suivi Scott Meyers :-)
Steve Jessop
11
witkamp a raison: en général, il est mauvais d'utiliser const_cast. C'est un cas spécifique où ce n'est pas le cas, comme l'explique Meyers. @Adam: ROM => const va bien. const == ROM est évidemment un non-sens puisque n'importe qui peut convertir non const en const à volonté: c'est équivalent à simplement choisir de ne pas modifier quelque chose.
Steve Jessop
44
En général, je suggère d'utiliser const_cast au lieu de static_cast pour ajouter const car cela vous empêche de changer le type accidentellement.
Greg Rogers
6
@HelloGoodbye: Je pense que Meyers suppose un minimum d'intelligence de la part du concepteur de l'interface de classe. Si get()constrenvoie quelque chose qui a été défini comme un objet const, alors il ne devrait pas y avoir de version non-const du get()tout. En fait, ma pensée à ce sujet a changé au fil du temps: la solution de modèle est le seul moyen d'éviter la duplication et d' obtenir une const-correct vérifiée par le compilateur, donc personnellement, je n'utiliserais plus un const_castpour éviter la duplication de code, je choisirais entre mettre le code dupé dans un modèle de fonction ou bien le laisser dupé.
Steve Jessop
7
Les deux modèles suivants contribuent énormément à la lisibilité de cette solution: template<typename T> const T& constant(T& _) { return const_cast<const T&>(_); }et template<typename T> T& variable(const T& _) { return const_cast<T&>(_); }. Ensuite, vous pouvez faire:return variable(constant(*this).get());
Casey Rodarmor
64

Oui, il est possible d'éviter la duplication de code. Vous devez utiliser la fonction membre const pour avoir la logique et demander à la fonction membre non const d'appeler la fonction membre const et de recréer la valeur de retour en une référence non const (ou un pointeur si les fonctions renvoient un pointeur):

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
};

Remarque: il est important que vous ne mettez pas la logique dans la fonction non-const et que la fonction const appelle la fonction non-const - cela peut entraîner un comportement non défini. La raison en est qu'une instance de classe constante est convertie en instance non constante. La fonction membre non const peut modifier accidentellement la classe, ce que les états standard C ++ entraîneront un comportement non défini.

Kevin
la source
3
Wow ... c'est horrible. Vous venez d'augmenter la quantité de code, de diminuer la clarté et d'ajouter deux const_cast <> s puants. Peut-être avez-vous un exemple en tête où cela a du sens?
Shog9
14
Hé ne fais pas ça!, C'est peut-être moche, mais selon Scott Meyers, c'est (presque) la bonne façon. Voir Effective C ++ , 3d ed, Item 3 sous le titre "Eviter la duplication dans les fonctions membres const et non-cost.
jwfearn
17
Bien que je comprenne que la solution peut être moche, imaginez que le code qui détermine ce qu'il faut retourner est de 50 lignes. Ensuite, la duplication est hautement indésirable - en particulier lorsque vous devez re-factoriser le code. Je l'ai rencontré plusieurs fois dans ma carrière.
Kevin
8
La différence entre cela et Meyers est que Meyers a static_cast <const X &> (* this). const_cast sert à supprimer const et non à l'ajouter.
Steve Jessop
8
@VioletGiraffe nous savons que l'objet n'a pas été créé à l'origine const, car il s'agit d'un membre non const d'un objet non const, ce que nous savons parce que nous sommes dans une méthode non const dudit objet. Le compilateur ne fait pas cette inférence, il suit une règle conservatrice. Pourquoi pensez-vous que const_cast existe, sinon pour ce genre de situation?
Caleth
47

C ++ 17 a mis à jour la meilleure réponse à cette question:

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

Cela présente les avantages suivants:

  • Est évident ce qui se passe
  • A une surcharge minimale de code - il tient dans une seule ligne
  • Est difficile de se tromper (ne peut être rejeté que volatilepar accident, mais volatileest un qualificatif rare)

Si vous souhaitez suivre la voie de déduction complète, cela peut être accompli en ayant une fonction d'aide

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;

Maintenant, vous ne pouvez même plus gâcher volatileet l'utilisation ressemble à

decltype(auto) f() const {
    return something_complicated();
}
decltype(auto) f() {
    return as_mutable(std::as_const(*this).f());
}
David Stone
la source
Notez que "as_mutable" avec la surcharge const rvalue supprimée (ce qui est généralement préférable) empêche le dernier exemple de fonctionner si f()renvoie à la Tplace de T&.
Max Truxa
1
@MaxTruxa: Oui, et c'est une bonne chose. Si elle venait d'être compilée, nous aurions une référence pendante. Dans le cas où les f()retours T, nous ne voulons pas avoir deux surcharges, la constversion seule est suffisante.
David Stone
Très vrai, je m'excuse pour mon pet de cerveau à fond hier, aucune idée de ce à quoi je pensais quand j'ai écrit ce commentaire. Je regardais une paire de getter const / mutable renvoyant a shared_ptr. Donc, ce dont j'avais réellement besoin était quelque chose as_mutable_ptrqui ressemble presque à ce qui as_mutableprécède, sauf qu'il prend et renvoie un shared_ptret utilise std::const_pointer_castau lieu de const_cast.
Max Truxa
1
Si une méthode retourne, T const*cela se lierait à T const* const&&plutôt que de se lier à T const* const&(du moins dans mes tests, il l'a fait). J'ai dû ajouter une surcharge pour T const*comme type d'argument pour les méthodes renvoyant un pointeur.
monkey0506
2
@ monkey0506: J'ai mis à jour ma réponse pour prendre en charge les pointeurs ainsi que les références
David Stone
34

Je pense que la solution de Scott Meyers peut être améliorée en C ++ 11 en utilisant une fonction d'assistance tempate. Cela rend l'intention beaucoup plus évidente et peut être réutilisé pour de nombreux autres getters.

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)...));
}

Cette fonction d'assistance peut être utilisée de la manière suivante.

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);
   }
};

Le premier argument est toujours le pointeur this. Le second est le pointeur sur la fonction membre à appeler. Après cela, une quantité arbitraire d'arguments supplémentaires peut être transmise afin qu'ils puissent être transmis à la fonction. Cela nécessite C ++ 11 en raison des modèles variadic.

Pait
la source
3
C'est dommage que nous n'ayons pas std::remove_bottom_constà y aller std::remove_const.
TBBle
Je n'aime pas cette solution car elle intègre toujours a const_cast. Vous pouvez créer getElementun modèle lui-même et utiliser le trait du type à l'intérieur pour les mpl::conditionaltypes dont vous avez besoin, comme iterators ou constiterators si nécessaire. Le vrai problème est de savoir comment générer une version const d'une méthode lorsque cette partie de la signature ne peut pas être modélisée?
v.oddou
2
@ v.oddou: std::remove_const<int const&>est int const &(supprimer la constqualification de haut niveau ), d'où la gymnastique NonConst<T>dans cette réponse. Le putatif std::remove_bottom_constpourrait supprimer la constqualification de bas niveau et faire exactement ce qui se NonConst<T>passe ici: std::remove_bottom_const<int const&>::type=> int&.
TBBle
4
Cette solution ne fonctionne pas bien si elle getElementest surchargée. Ensuite, le pointeur de fonction ne peut pas être résolu sans donner explicitement les paramètres du modèle. Pourquoi?
John
1
Vous devez corriger votre réponse pour utiliser le transfert parfait C ++ 11: likeConstVersion(TObj const* obj, TConstReturn (TObj::*memFun)(TArgs...) const, TArgs&&... args) { return const_cast<typename NonConst<TConstReturn>::type>((obj->*memFun)(std::forward<TArgs>(args)...)); }Complete: gist.github.com/BlueSolei/bca26a8590265492e2f2760d3cefcf83
ShaulF
22

Un peu plus verbeux que Meyers, mais je pourrais faire ceci:

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); }
};

La méthode private a la propriété indésirable de renvoyer un Z & non const pour une instance const, c'est pourquoi elle est privée. Les méthodes privées peuvent casser les invariants de l'interface externe (dans ce cas, l'invariant souhaité est "un objet const ne peut pas être modifié via des références obtenues via lui vers les objets qu'il a-a").

Notez que les commentaires font partie du modèle - l'interface de _getZ spécifie qu'il n'est jamais valide de l'appeler (à part les accesseurs, évidemment): il n'y a aucun avantage concevable à le faire de toute façon, car c'est un caractère de plus à taper et ne le fera pas entraîner un code plus petit ou plus rapide. Appeler la méthode équivaut à appeler l'un des accesseurs avec un const_cast, et vous ne voudriez pas le faire non plus. Si vous craignez de rendre les erreurs évidentes (et c'est un objectif équitable), appelez-le const_cast_getZ au lieu de _getZ.

Soit dit en passant, j'apprécie la solution de Meyers. Je n'ai aucune objection philosophique à lui. Personnellement, cependant, je préfère un tout petit peu de répétition contrôlée et une méthode privée qui ne doit être appelée que dans certaines circonstances étroitement contrôlées, plutôt qu'une méthode qui ressemble à du bruit de ligne. Choisissez votre poison et respectez-le.

[Edit: Kevin a souligné à juste titre que _getZ pourrait vouloir appeler une autre méthode (disons generateZ) qui est const-spécialisée de la même manière que getZ. Dans ce cas, _getZ verrait un const Z & et devrait le const_cast avant de revenir. C'est toujours sûr, car l'accessoire passe-partout règle tout, mais il n'est pas extrêmement évident qu'il soit sûr. De plus, si vous faites cela et que vous modifiez ensuite generateZ pour toujours retourner const, vous devez également changer getZ pour toujours retourner const, mais le compilateur ne vous le dira pas.

Ce dernier point sur le compilateur est également vrai pour le modèle recommandé par Meyers, mais le premier point sur un const_cast non évident ne l'est pas. Donc, tout compte fait, je pense que si _getZ s'avère avoir besoin d'un const_cast pour sa valeur de retour, alors ce modèle perd beaucoup de sa valeur par rapport à Meyers. Puisqu'il souffre également d'inconvénients par rapport à Meyers, je pense que je passerais au sien dans cette situation. La refactorisation de l'un à l'autre est facile - elle n'affecte aucun autre code valide de la classe, car seul le code non valide et le passe-partout appellent _getZ.]

Steve Jessop
la source
3
Cela a toujours le problème que la chose que vous renvoyez peut être constante pour une instance constante de X. Dans ce cas, vous avez toujours besoin d'un const_cast dans _getZ (...). S'il est mal utilisé par les développeurs ultérieurs, il peut toujours conduire à UB. Si la chose retournée est «mutable», alors c'est une bonne solution.
Kevin
1
Toute fonction privée (diable, publique aussi) peut être mal utilisée par les développeurs ultérieurs, s'ils choisissent d'ignorer les instructions BLOCK CAPITAL sur son utilisation valide, dans le fichier d'en-tête et aussi dans Doxygen etc. Je ne peux pas arrêter cela, et je ne le considère pas comme mon problème car les instructions sont faciles à comprendre.
Steve Jessop
13
-1: Cela ne fonctionne pas dans de nombreuses situations. Et si somethingdans la _getZ()fonction se trouve une variable d'instance? Le compilateur (ou au moins certains compilateurs) se plaindra du fait que puisque _getZ()c'est const, toute variable d'instance référencée à l'intérieur l'est aussi const. Il en somethingserait de même pour const (il serait de type const Z&) et ne pourrait pas être converti en Z&. D'après mon expérience (certes quelque peu limitée), la plupart du temps somethingest une variable d'instance dans des cas comme celui-ci.
Gravity
2
@GravityBringer: alors "quelque chose" doit impliquer a const_cast. Il était destiné à être un espace réservé pour le code requis pour obtenir un retour non const de l'objet const, et non pas un espace réservé pour ce qui aurait été dans le getter dupliqué. Donc, "quelque chose" n'est pas seulement une variable d'instance.
Steve Jessop
2
Je vois. Cela diminue cependant l'utilité de la technique. Je supprimerais le downvote, mais SO ne me le permettra pas.
Gravity
22

Belle question et belles réponses. J'ai une autre solution, qui n'utilise aucun casting:

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);
    }

};

Cependant, il a la laideur d'exiger un membre statique et la nécessité d'utiliser la instancevariable à l'intérieur.

Je n'ai pas considéré toutes les implications (négatives) possibles de cette solution. Faites-le moi savoir s'il y en a.

gd1
la source
4
Eh bien, laisse aller avec le simple fait que vous avez ajouté plus de passe-partout. Si quoi que ce soit, cela devrait être utilisé comme un exemple de la raison pour laquelle le langage a besoin d'un moyen de modifier les qualificateurs de fonction avec le type de retour auto get(std::size_t i) -> auto(const), auto(&&). Pourquoi '&&'? Ahh, donc je peux dire:auto foo() -> auto(const), auto(&&) = delete;
kfsone
gd1: exactement ce que j'avais en tête. @kfsone et exactement ce que j'ai conclu aussi.
v.oddou
1
@kfsone la syntaxe devrait incorporer un thismot-clé. Je suggère que template< typename T > auto myfunction(T this, t args) -> decltype(ident)le mot-clé this soit reconnu comme l'argument d'instance d'objet implicite et que le compilateur reconnaisse que mafonction est un membre ou T. Tsera déduit automatiquement sur le site de l'appel, qui sera toujours le type de la classe, mais avec une qualification cv gratuite.
v.oddou
2
Cette solution a également l'avantage (par rapport à const_castcelle) de permettre de revenir iteratoret const_iterator.
Jarod42
1
Si l'implémentation est déplacée dans le fichier cpp (et comme la méthode à ne pas dupliquer ne devrait pas être triviale, ce serait probablement le cas), cela staticpeut être fait à la portée du fichier au lieu de la portée de la classe. :-)
Jarod42
8

Vous pouvez également résoudre ce problème avec des modèles. Cette solution est légèrement laide (mais la laideur est cachée dans le fichier .cpp) mais elle permet au compilateur de vérifier la constance, et pas de duplication de code.

Fichier .h:

#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;
};

Fichier .cpp:

#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 );
}

Le principal inconvénient que je peux voir est que parce que toute l'implémentation complexe de la méthode est dans une fonction globale, vous devez soit saisir les membres de X en utilisant des méthodes publiques comme GetVector () ci-dessus (dont il doit toujours y avoir un version const et non const) ou vous pourriez faire de cette fonction un ami. Mais je n'aime pas les amis.

[Modifier: suppression de l'inclusion inutile de cstdio ajoutée pendant le test.]

Andy Balaam
la source
3
Vous pouvez toujours faire de la fonction d'implémentation complexe un membre statique pour accéder aux membres privés. La fonction ne doit être déclarée que dans le fichier d'en-tête de classe, la définition peut résider dans le fichier d'implémentation de classe. Cela fait, après tout, partie de l'implémentation de la classe.
CB Bailey
Aah oui bonne idée! Je n'aime pas les trucs de modèle apparaissant dans l'en-tête, mais si, ici, cela rend potentiellement l'implémentation beaucoup plus simple, cela en vaut probablement la peine.
Andy Balaam
+ 1 à cette solution qui ne ferait pas double emploi code, ni utilise une vilaine const_cast(qui pourrait accidentellement être utilisé pour quelque chose qui est canst effectivement censé être const à quelque chose qui est pas).
HelloGoodbye
De nos jours, cela peut être simplifié avec un type de retour déduit pour le modèle (particulièrement utile car il réduit ce qui doit être dupliqué dans la classe dans le cas membre).
Davis Herring
3

Que diriez-vous de déplacer la logique dans une méthode privée, et de ne faire que les choses "obtenir la référence et retourner" à l'intérieur des getters? En fait, je serais assez confus au sujet des transtypages statiques et const à l'intérieur d'une simple fonction getter, et je considérerais cela laid, sauf dans des circonstances extrêmement rares!

MP24
la source
Afin d'éviter un comportement indéfini, vous avez toujours besoin d'un const_cast. Voir la réponse de Martin York et mon commentaire là-bas.
Kevin
1
Kevin, quelle réponse de Martin York
Peter Nimmo
2

Est-ce de la triche d'utiliser le préprocesseur?

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

};

Ce n'est pas aussi sophistiqué que les modèles ou les transtypages, mais cela rend votre intention ("ces deux fonctions doivent être identiques") assez explicite.

user1476176
la source
1
Mais alors vous devez être prudent avec les barres obliques inverses (comme d'habitude pour les macros multilignes) et en plus vous perdez la coloration syntaxique dans la plupart (sinon la totalité) des éditeurs.
Ruslan
2

C'est surprenant pour moi qu'il y ait tant de réponses différentes, mais presque toutes reposent sur une magie de modèle lourde. Les modèles sont puissants, mais parfois les macros les battent avec concision. La polyvalence maximale est souvent obtenue en combinant les deux.

J'ai écrit une macro FROM_CONST_OVERLOAD()qui peut être placée dans la fonction non-const pour invoquer la fonction const.

Exemple d'utilisation:

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) );
    }
};

Implémentation simple et réutilisable:

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)

Explication:

Comme indiqué dans de nombreuses réponses, le modèle typique pour éviter la duplication de code dans une fonction membre non const est le suivant:

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

Une grande partie de ce passe-partout peut être évitée en utilisant l'inférence de type. Tout d'abord, const_castpeut être encapsulé dans WithoutConst(), ce qui infère le type de son argument et supprime le qualificatif const. Deuxièmement, une approche similaire peut être utilisée WithConst()pour qualifier const le thispointeur, ce qui permet d'appeler la méthode const-overloaded.

Le reste est une simple macro qui préfixe l'appel avec le bon qualifié this->et supprime const du résultat. Étant donné que l'expression utilisée dans la macro est presque toujours un simple appel de fonction avec des arguments transmis 1: 1, les inconvénients des macros telles que l'évaluation multiple ne se déclenchent pas. Les points de suspension et __VA_ARGS__peuvent également être utilisés, mais ne devraient pas être nécessaires car des virgules (comme les séparateurs d'arguments) apparaissent entre parenthèses.

Cette approche présente plusieurs avantages:

  • Syntaxe minimale et naturelle - il suffit d'envelopper l'appel FROM_CONST_OVERLOAD( )
  • Aucune fonction de membre supplémentaire requise
  • Compatible avec C ++ 98
  • Implémentation simple, pas de métaprogrammation de modèle et aucune dépendance
  • Extensible: d' autres relations de const peuvent être ajoutés (comme const_iterator, std::shared_ptr<const T>, etc.). Pour cela, il suffit de surcharger WithoutConst()les types correspondants.

Limitations: cette solution est optimisée pour les scénarios où la surcharge non const fait exactement la même chose que la surcharge const, de sorte que les arguments peuvent être transmis 1: 1. Si votre logique diffère et que vous n'appelez pas la version const via this->Method(args), vous pouvez envisager d'autres approches.

L'opérateur
la source
2

Pour ceux (comme moi) qui

  • utiliser c ++ 17
  • voulez ajouter le moins de passe-partout / répétition et
  • ça ne me dérange pas d'utiliser des macros (en attendant les méta-classes ...),

voici une autre prise:

#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)...));              \
    }

Il s'agit essentiellement d'un mélange des réponses de @Pait, @DavidStone et @ sh1 ( EDIT : et une amélioration de @cdhowie). Ce qu'il ajoute au tableau, c'est que vous vous en sortez avec une seule ligne de code supplémentaire qui nomme simplement la fonction (mais pas d'argument ou de duplication de type de retour):

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

Remarque: gcc ne parvient pas à compiler cela avant 8.1, clang-5 et plus ainsi que MSVC-19 sont satisfaits (selon l'explorateur du compilateur ).

axxel
la source
Cela a fonctionné directement pour moi. Ceci est une excellente réponse, merci!
Court
Les decltype()s ne devraient-ils pas également être utilisés std::forwardsur les arguments pour s'assurer que nous utilisons le bon type de retour dans le cas où nous avons des surcharges get()qui prennent différents types de références?
cdhowie
@cdhowie Pouvez-vous donner un exemple?
axxel
@axxel Il est arrangea comme l' enfer, mais ici vous allez . La NON_CONSTmacro déduit le type de retour de manière incorrecte et const_casts au mauvais type en raison du manque de transfert dans les decltype(func(a...))types. Les remplacer par decltype(func(std::forward<T>(a)...)) résout ce problème . (Il y a juste une erreur de l'éditeur de liens car je n'ai jamais défini de X::getsurcharge déclarée .)
cdhowie
1
Merci @cdhowie, j'ai pimpé votre exemple pour utiliser réellement les surcharges non const: coliru.stacked-crooked.com/a/0cedc7f4e789479e
axxel
1

Voici une version C ++ 17 de la fonction d'assistance statique du modèle, avec et un test SFINAE facultatif.

#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); }
};

Version complète: https://godbolt.org/z/mMK4r3

atablash
la source
1

J'ai trouvé une macro qui génère automatiquement des paires de fonctions const / non const.

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;}
};

Voir la fin de la réponse pour l'implémentation.

L'argument de MAYBE_CONSTest dupliqué. Dans la première copie, CVest remplacé par rien; et dans le deuxième exemplaire, il est remplacé par const.

Il n'y a pas de limite au nombre de fois qui CVpeuvent apparaître dans l'argument macro.

Il y a cependant un léger inconvénient. Si CVapparaît entre parenthèses, cette paire de parenthèses doit être préfixée avec 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 &); )

La mise en oeuvre:

#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)

Implémentation pré-C ++ 20 qui ne prend pas en charge 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
la source
0

En règle générale, les fonctions membres pour lesquelles vous avez besoin de versions const et non const sont des getters et setters. La plupart du temps, il s'agit de lignes uniques, la duplication de code n'est donc pas un problème.

Dima
la source
2
C'est peut-être vrai la plupart du temps. Mais il y a des exceptions.
Kevin
1
obtient de toute façon, un passeur de const n'a pas beaucoup de sens;)
jwfearn
Je voulais dire que le getter non-const est effectivement un setter. :)
Dima
0

J'ai fait cela pour un ami qui a justifié à juste titre l'utilisation de const_cast... sans le savoir, j'aurais probablement fait quelque chose comme ça (pas vraiment élégant):

#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
la source
0

Je suggère un modèle de fonction statique d'assistance privée, comme ceci:

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
la source
-1

Cet article DDJ montre une manière d'utiliser la spécialisation de modèle qui ne vous oblige pas à utiliser const_cast. Pour une fonction aussi simple, elle n'est cependant pas vraiment nécessaire.

boost :: any_cast (à un moment donné, il n'en a plus) utilise un const_cast de la version const appelant la version non-const pour éviter la duplication. Vous ne pouvez pas imposer de sémantique const à la version non-const, donc vous devez être très prudent avec cela.

En fin de compte, une certaine duplication de code est correcte tant que les deux extraits sont directement l'un sur l'autre.

Greg Rogers
la source
L'article DDJ semble faire référence aux itérateurs - ce qui n'est pas pertinent pour la question. Les const-itérateurs ne sont pas des données constantes - ce sont des itérateurs qui pointent vers des données constantes.
Kevin
-1

Pour ajouter à la solution jwfearn et kevin fournie, voici la solution correspondante lorsque la fonction retourne shared_ptr:

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
la source
-1

Je n'ai pas trouvé ce que je cherchais, alors j'en ai roulé quelques-uns ...

Celui-ci est un peu verbeux, mais a l'avantage de gérer de nombreuses méthodes surchargées du même nom (et type de retour) à la fois:

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...));
  }
};

Si vous n'avez qu'une seule constméthode par nom, mais encore beaucoup de méthodes à dupliquer, vous préférerez peut-être ceci:

  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); }

Malheureusement, cela tombe en panne dès que vous commencez à surcharger le nom (la liste des arguments de l'argument du pointeur de fonction semble être non résolue à ce stade, donc il ne peut pas trouver de correspondance pour l'argument de fonction). Bien que vous puissiez également définir un moyen de vous en sortir:

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

Mais les arguments de référence à la constméthode ne correspondent pas aux arguments apparemment par valeur du modèle et il se casse. Pas certain de pourquoi. Voici pourquoi .

sh1
la source