web-dev-qa-db-fra.com

Paramètres de fonction facultatifs: utiliser des arguments par défaut (NULL) ou surcharger la fonction?

J'ai une fonction qui traite un vecteur donné, mais peut également créer un tel vecteur lui-même s'il n'est pas donné.

Je vois deux choix de conception pour un tel cas, où un paramètre de fonction est facultatif:

Faites-en un pointeur et faites-le NULL par défaut:

void foo(int i, std::vector<int>* optional = NULL) {
  if(optional == NULL){
    optional = new std::vector<int>();
    // fill vector with data
  }
  // process vector
}

Ou avoir deux fonctions avec un nom surchargé, dont l'une laisse de côté l'argument:

void foo(int i) {
   std::vector<int> vec;
   // fill vec with data
   foo(i, vec);
}

void foo(int i, const std::vector<int>& optional) {
  // process vector
}

Y a-t-il des raisons de préférer une solution à l'autre?

Je préfère légèrement le second parce que je peux faire du vecteur une référence const, car il est, lorsqu'il est fourni, uniquement lu, non écrit. De plus, l'interface semble plus propre (n'est-ce pas NULL juste un hack?). Et la différence de performances résultant de l'appel de fonction indirect est probablement optimisée.

Pourtant, je vois souvent la première solution dans le code. Y a-t-il des raisons impérieuses de le préférer, à part la paresse des programmeurs?

41
Frank

Je préférerais certainement la 2ème approche des méthodes surchargées.

La première approche (paramètres facultatifs) brouille la définition de la méthode car elle n'a plus un seul objectif bien défini. Cela augmente à son tour la complexité du code, ce qui rend plus difficile pour quelqu'un qui ne le connaît pas de le comprendre.

Avec la deuxième approche (méthodes surchargées), chaque méthode a un objectif clair. Chaque méthode est bien structurée et cohérente . Quelques notes supplémentaires:

  • S'il y a du code qui doit être dupliqué dans les deux méthodes, cela peut être extrait dans une méthode distincte et chaque méthode surchargée pourrait appeler cette méthode externe.
  • Je voudrais aller plus loin et nommer chaque méthode différemment pour indiquer les différences entre les méthodes. Cela rendra le code plus auto-documenté.
28

Je n'utiliserais aucune des deux approches.

Dans ce contexte, le but de foo () semble être de traiter un vecteur. Autrement dit, le travail de foo () consiste à traiter le vecteur.

Mais dans la deuxième version de foo (), on lui donne implicitement un second travail: créer le vecteur. La sémantique entre foo () version 1 et foo () version 2 n'est pas la même.

Au lieu de faire cela, j'envisagerais d'avoir une seule fonction foo () pour traiter un vecteur, et une autre fonction qui crée le vecteur, si vous en avez besoin.

Par exemple:

void foo(int i, const std::vector<int>& optional) {
  // process vector
}

std::vector<int>* makeVector() {
   return new std::vector<int>;
}

Évidemment, ces fonctions sont triviales, et si tout ce que makeVector () a besoin pour faire son travail est littéralement d'appeler new, alors il peut être inutile d'avoir la fonction makeVector (). Mais je suis sûr que dans votre situation actuelle, ces fonctions font bien plus que ce qui est montré ici, et mon code ci-dessus illustre une approche fondamentale de la conception sémantique: donner à une fonction un travail à faire.

La conception que j'ai ci-dessus pour la fonction foo () illustre également une autre approche fondamentale que j'utilise personnellement dans mon code quand il s'agit de concevoir des interfaces - qui comprend des signatures de fonction, des classes, etc. Voilà: je crois que ne bonne interface est 1) facile et intuitive à utiliser correctement, et 2) difficile ou impossible à utiliser incorrectement. Dans le cas de la fonction foo (), nous disons implicitement qu'avec ma conception, le vecteur doit déjà exister et être "prêt". En concevant foo () pour prendre une référence au lieu d'un pointeur, il est à la fois intuitif que l'appelant doit déjà avoir un vecteur, et ils auront du mal à passer quelque chose qui n'est pas un vecteur prêt à l'emploi .

42
John Dibling

Bien que je comprenne les plaintes de nombreuses personnes concernant les paramètres par défaut et les surcharges, il semble y avoir un manque de compréhension des avantages de ces fonctionnalités.

Valeurs des paramètres par défaut:
Tout d'abord, je tiens à souligner que lors de la conception initiale d'un projet, les défauts devraient être peu ou pas utilisés s'ils sont bien conçus. Cependant, là où les plus grands atouts des valeurs par défaut entrent en jeu, ce sont les projets existants et les API bien établies. Je travaille sur des projets qui se composent de millions de lignes de code existantes et n'ont pas le luxe de tous les recoder. Ainsi, lorsque vous souhaitez ajouter une nouvelle fonctionnalité qui nécessite un paramètre supplémentaire; une valeur par défaut est nécessaire pour le nouveau paramètre. Sinon, vous casserez tous ceux qui utilisent votre projet. Ce qui me conviendrait personnellement, mais je doute que votre entreprise ou les utilisateurs de votre produit/API apprécieraient d'avoir à recoder leurs projets à chaque mise à jour. Simplement, les valeurs par défaut sont idéales pour la compatibilité descendante! C'est généralement la raison pour laquelle vous verrez des valeurs par défaut dans les grandes API ou les projets existants.

Fonction Overrides: L'avantage des fonctions overrides est qu'elles permettent le partage d'un concept de fonctionnalité, mais avec des options/paramètres différents. Cependant, plusieurs fois, je vois des remplacements de fonctions utilisés paresseusement pour fournir des fonctionnalités radicalement différentes, avec des paramètres légèrement différents. Dans ce cas, ils doivent chacun avoir des fonctions nommées séparément, se rapportant à leur fonctionnalité spécifique (comme avec l'exemple de l'OP).

Ces fonctionnalités de c/c ++ sont bonnes et fonctionnent bien lorsqu'elles sont utilisées correctement. Ce qui peut être dit de la plupart des fonctionnalités de programmation. C'est lorsqu'ils sont maltraités/maltraités qu'ils causent des problèmes.

Avertissement:
.

21
David Ruhmann

Une référence ne peut pas être NULL en C++, une très bonne solution serait d'utiliser un modèle Nullable. Cela vous permettrait de faire des choses est ref.isNull ()

Ici, vous pouvez utiliser ceci:

template<class T>
class Nullable {
public:
    Nullable() {
        m_set = false;
    }
    explicit
    Nullable(T value) {
        m_value = value;
        m_set = true;
    }
    Nullable(const Nullable &src) {
        m_set = src.m_set;
        if(m_set)
            m_value = src.m_value;
    }
    Nullable & operator =(const Nullable &RHS) {
        m_set = RHS.m_set;
        if(m_set)
            m_value = RHS.m_value;
        return *this;
    }
    bool operator ==(const Nullable &RHS) const {
        if(!m_set && !RHS.m_set)
            return true;
        if(m_set != RHS.m_set)
            return false;
        return m_value == RHS.m_value;
    }
    bool operator !=(const Nullable &RHS) const {
        return !operator==(RHS);
    }

    bool GetSet() const {
        return m_set;
    }

    const T &GetValue() const {
        return m_value;
    }

    T GetValueDefault(const T &defaultValue) const {
        if(m_set)
            return m_value;
        return defaultValue;
    }
    void SetValue(const T &value) {
        m_value = value;
        m_set = true;
    }
    void Clear()
    {
        m_set = false;
    }

private:
    T m_value;
    bool m_set;
};

Vous pouvez maintenant avoir

void foo(int i, Nullable<AnyClass> &optional = Nullable<AnyClass>()) {
   //you can do 
   if(optional.isNull()) {

   }
}
6
Sid Sarasvati

Je suis d'accord, j'utiliserais deux fonctions. Fondamentalement, vous avez deux cas d'utilisation différents, il est donc logique d'avoir deux implémentations différentes.

Je trouve que plus j'écris de code C++, moins j'ai de paramètres par défaut - je ne verserais pas vraiment de larmes si la fonctionnalité était obsolète, même si je devrais réécrire une charge perdue de l'ancien code!

5
anon

J'évite généralement le premier cas. Notez que ces deux fonctions sont différentes dans ce qu'elles font. L'un d'eux remplit un vecteur avec quelques données. L'autre ne l'accepte pas (accepte simplement les données de l'appelant). J'ai tendance à nommer différemment des fonctions qui font réellement des choses différentes. En fait, même lorsque vous les écrivez, ce sont deux fonctions:

  • foo_default (ou simplement foo)
  • foo_with_values

Au moins, je trouve cette distinction plus propre dans le long therm, et pour l'utilisateur occasionnel de bibliothèque/fonctions.

3
Diego Sevilla

En C++, vous devez éviter d'autoriser autant que possible les paramètres NULL valides. La raison en est qu'elle réduit considérablement la documentation du site d'appel. Je sais que cela semble extrême, mais je travaille avec des API qui prennent plus de 10 à 20 paramètres, dont la moitié peut être NULL. Le code résultant est presque illisible

SomeFunction(NULL, pName, NULL, pDestination);

Si vous deviez le basculer pour forcer les références const, le code est simplement forcé d'être plus lisible.

SomeFunction(
  Location::Hidden(),
  pName,
  SomeOtherValue::Empty(),
  pDestination);
2
JaredPar

Je suis carrément dans le camp de "surcharge". D'autres ont ajouté des détails sur votre exemple de code réel, mais je voulais ajouter ce que je pense être les avantages de l'utilisation des surcharges par rapport aux valeurs par défaut pour le cas général.

  • Tout paramètre peut être "par défaut"
  • Pas de problème si une fonction prioritaire utilise une valeur différente pour sa valeur par défaut.
  • Il n'est pas nécessaire d'ajouter des constructeurs "hacky" aux types existants afin de leur permettre d'avoir la valeur par défaut.
  • Les paramètres de sortie peuvent être définis par défaut sans avoir besoin d'utiliser des pointeurs ou des objets globaux hacky.

Pour mettre des exemples de code sur chacun:

Tout paramètre peut être défini par défaut:

class A {}; class B {}; class C {};

void foo (A const &, B const &, C const &);

inline void foo (A const & a, C const & c)
{
  foo (a, B (), c);    // 'B' defaulted
}

Pas de danger de passer outre les fonctions ayant des valeurs différentes pour la valeur par défaut:

class A {
public:
  virtual void foo (int i = 0);
};

class B : public A {
public:
  virtual void foo (int i = 100);
};


void bar (A & a)
{
  a.foo ();           // Always uses '0', no matter of dynamic type of 'a'
}

Il n'est pas nécessaire d'ajouter des constructeurs "hacky" aux types existants pour leur permettre d'être par défaut:

struct POD {
  int i;
  int j;
};

void foo (POD p);     // Adding default (other than {0, 0})
                      // would require constructor to be added
inline void foo ()
{
  POD p = { 1, 2 };
  foo (p);
}

Les paramètres de sortie peuvent être définis par défaut sans avoir besoin d'utiliser des pointeurs ou des objets globaux hacky:

void foo (int i, int & j);  // Default requires global "dummy" 
                            // or 'j' should be pointer.
inline void foo (int i)
{
  int j;
  foo (i, j);
}

La seule exception à la règle concernant la surcharge par rapport aux valeurs par défaut est pour les constructeurs où il n'est actuellement pas possible pour un constructeur de transmettre à un autre. (Je crois que C++ 0x résoudra cela cependant).

2
Richard Corden

Moi aussi, je préfère le second. Bien qu'il n'y ait pas beaucoup de différence entre les deux, vous êtes fondamentalement en utilisant la fonctionnalité de la méthode principale dans la surcharge foo(int i) et la surcharge primaire fonctionneraient parfaitement sans se soucier de l'existence d'un manque de l'autre, il y a donc plus de séparation des préoccupations dans la version de surcharge.

2
Mehrdad Afshari

Je préférerais une troisième option: Séparer en deux fonctions, mais ne pas surcharger.

Les surcharges, par nature, sont moins utilisables. Ils obligent l'utilisateur à prendre conscience de deux options et à comprendre quelle est la différence entre eux, et s'ils sont si enclins, à vérifier également la documentation ou le code pour s'assurer lequel est lequel.

J'aurais une fonction qui prend le paramètre, et une qui s'appelle "createVectorAndFoo" ou quelque chose comme ça (évidemment, le nommage devient plus facile avec de vrais problèmes).

Bien que cela viole la règle des "deux responsabilités pour la fonction" (et lui donne un nom long), je pense que cela est préférable lorsque votre fonction fait vraiment deux choses (créer un vecteur et le foo).

1
Uri

En général, je suis d'accord avec la suggestion des autres d'utiliser une approche à deux fonctions. Cependant, si le vecteur créé lorsque le formulaire à 1 paramètre est utilisé est toujours le même, vous pouvez simplifier les choses en le rendant statique et en utilisant un const& paramètre à la place:

// Either at global scope, or (better) inside a class
static vector<int> default_vector = populate_default_vector();

void foo(int i, std::vector<int> const& optional = default_vector) {
    ...
}
1
j_random_hacker

La première façon est moins bonne car vous ne pouvez pas dire si vous avez accidentellement passé NULL ou si cela a été fait exprès ... si c'était un accident, alors vous avez probablement causé un bug.

Avec le second, vous pouvez tester (affirmer, peu importe) pour NULL et le gérer de manière appropriée.

0
TofuBeer