web-dev-qa-db-fra.com

Méthodes d'extension en c ++

Je cherchais une implémentation de méthodes d'extension en c ++ et suis tombé sur cette discussion comp.std.c ++ qui mentionne que polymorphic_map peut être utilisé pour associer des méthodes à une classe, mais le lien fourni semble être mort. Quelqu'un sait-il à quoi cette réponse faisait référence, ou s'il existe une autre façon d'étendre les classes d'une manière similaire aux méthodes d'extension (peut-être en utilisant des mixins?).

Je sais que la solution canonique C++ consiste à utiliser des fonctions libres; c'est plus par curiosité qu'autre chose.

58
Bob

Différentes langues abordent le développement de différentes manières. En particulier C # et Java ont un point de vue fort par rapport à OO qui mène à tout est un objet état d'esprit (C # est un peu plus laxiste ici.) Dans cette approche, les méthodes d'extension fournissent un moyen simple d'étendre un objet ou une interface existante pour ajouter de nouvelles fonctionnalités.

Il n'y a pas de méthodes d'extension en C++, ni nécessaires. Lors du développement de C++, oubliez que tout est un paradigme d'objet - qui, soit dit en passant, est faux même en Java/C # [*]. Un état d'esprit différent est pris en C++, il y a des objets et les objets ont des opérations qui font intrinsèquement partie de l'objet, mais il y a aussi d'autres opérations qui font partie de l'interface et n'ont pas besoin de faire partie de la classe. Un incontournable à lire par Herb Sutter est What's In a Class? , où l'auteur défend (et je suis d'accord) que vous pouvez facilement étendre n'importe quelle classe avec de simples fonctions gratuites.

À titre d'exemple simple, la classe de modèle standard basic_ostream A quelques méthodes membres pour vider le contenu de certains types primitifs, puis elle est améliorée avec des fonctions libres (également basées sur un modèle) qui étendent cette fonctionnalité à d'autres types en en utilisant l'interface publique existante. Par exemple, std::cout << 1; Est implémenté en tant que fonction membre, tandis que std::cout << "Hi"; Est une fonction gratuite implémentée en termes d'autres membres plus basiques.

L'extensibilité en C++ est obtenue au moyen de fonctions gratuites, et non par l'ajout de nouvelles méthodes aux objets existants.

[*] Tout n'est pas un objet.

Dans un domaine donné contiendra un ensemble d'objets réels qui peuvent être modélisés et des opérations qui peuvent leur être appliquées, dans certains cas, ces opérations feront partie de l'objet, mais dans certains autres cas, elles ne le seront pas. En particulier, vous trouverez classes utilitaires dans les langues qui prétendent que tout est un objet et ces classes utilitaires ne sont rien d'autre qu'un calque essayant de cacher le fait que ces méthodes n'appartiennent à aucun objet particulier.

Même certaines opérations implémentées en tant que fonctions membres ne sont pas vraiment des opérations sur l'objet. Envisagez d'ajouter une classe de nombres Complex, en quoi sum (ou +) Est-elle plus une opération sur le premier argument que sur le second? Pourquoi a.sum(b); ou b.sum(a), ne devrait-il pas être sum( a, b )?

Forcer les opérations à être des méthodes membres produit en fait des effets étranges - mais nous y sommes habitués: a.equals(b); et b.equals(a); peuvent avoir des résultats complètement différents même si l'implémentation de equals est entièrement symétrique. (Considérez ce qui se passe lorsque a ou b est un pointeur nul)

L'approche de Boost Range Library utilise l'opérateur | ().

r | filtered(p);

Je peux également écrire le trim pour la chaîne de la même manière.

#include <string>

namespace string_extension {

struct trim_t {
    std::string operator()(const std::string& s) const
    {
        ...
        return s;
    }
};

const trim_t trim = {};

std::string operator|(const std::string& s, trim_t f)
{
    return f(s);
}

} // namespace string_extension

int main()
{
    const std::string s = "  abc  ";

    const std::string result = s | string_extension::trim;
}
24
Akira Takahashi

La réponse courte est que vous ne pouvez pas faire cela. La réponse longue est que vous pouvez le simuler, mais sachez que vous devrez créer beaucoup de code comme solution de contournement (en fait, je ne pense pas qu'il existe une solution élégante).

Dans la discussion, une solution de contournement très complexe est fournie en utilisant operator- (ce qui est une mauvaise idée, à mon avis). Je suppose que la solution fournie dans le lien mort était plus ou moins similaire (car elle était basée sur l'opérateur |).

Ceci est basé sur la capacité de pouvoir faire plus ou moins la même chose qu'une méthode d'extension avec des opérateurs. Par exemple, si vous souhaitez surcharger l'opérateur ostream << pour votre nouvelle classe Foo, vous pouvez faire:

class Foo {
    friend ostream &operator<<(ostream &o, const Foo &foo);
    // more things...
};

ostream &operator<<(ostream &o, const Foo &foo)
{
  // write foo's info to o
}

Comme je l'ai dit, c'est le seul mécanisme similaire disponible en C++ pour les méthodes d'extension. Si vous pouvez naturellement traduire votre fonction en un opérateur surchargé, alors ça va. La seule autre possibilité est de surcharger artificiellement un opérateur qui n'a rien à voir avec votre objectif, mais cela vous fera écrire du code très déroutant.

L'approche la plus similaire à laquelle je peux penser consisterait à créer une classe d'extension et à y créer vos nouvelles méthodes. Malheureusement, cela signifie que vous devrez "adapter" vos objets:

class stringext {
public:
    stringext(std::string &s) : str( &s )
        {}
    string trim()
        {  ...; return *str; }
private:
    string * str;
};

Et puis, quand vous voulez faire ça:

void fie(string &str)
{
    // ...
    cout << stringext( str ).trim() << endl;
}

Comme je l'ai dit, ce n'est pas parfait, et je ne pense pas que ce type de solution parfaite existe. Désolé.

7
Baltasarq

C'est la chose la plus proche que j'ai jamais vue des méthodes d'extension en C++. Personnellement, j'aime la façon dont il peut être utilisé, et peut-être que c'est le plus proche possible des méthodes d'extension dans cette langue. Mais il y a quelques inconvénients:

  • Il peut être compliqué à mettre en œuvre
  • La priorité de l'opérateur peut ne pas être aussi agréable à certains moments, cela peut provoquer des surprises

Une solution:

#include <iostream>

using namespace std;


class regular_class {

    public:

        void simple_method(void) const {
            cout << "simple_method called." << endl;
        }

};


class ext_method {

    private:

        // arguments of the extension method
        int x_;

    public:

        // arguments get initialized here
        ext_method(int x) : x_(x) {

        }


        // just a dummy overload to return a reference to itself
        ext_method& operator-(void) {
            return *this;
        }


        // extension method body is implemented here. The return type of this op. overload
        //    should be the return type of the extension method
        friend const regular_class& operator<(const regular_class& obj, const ext_method& mthd) {

            cout << "Extension method called with: " << mthd.x_ << " on " << &obj << endl;
            return obj;
        }
};


int main()
{ 
    regular_class obj;
    cout << "regular_class object at: " << &obj << endl;
    obj.simple_method();
    obj<-ext_method(3)<-ext_method(8);
    return 0;
}

Ce n'est pas mon invention personnelle, récemment un de mes amis me l'a envoyée par la poste, il a dit l'avoir obtenue d'une liste de diffusion universitaire.

7
dennis90

Pour en savoir plus sur la réponse @Akira, operator| peut être utilisé pour étendre des classes existantes avec des fonctions qui prennent aussi des paramètres. Voici un exemple que j'utilise pour étendre la bibliothèque XML de Xerces avec des fonctionnalités de recherche qui peuvent être facilement concaténées:

#pragma once

#include <string>
#include <stdexcept>

#include <xercesc/dom/DOMElement.hpp>

#define _U16C // macro that converts string to char16_t array

XERCES_CPP_NAMESPACE_BEGIN
    struct FindFirst
    {
        FindFirst(const std::string& name);
        DOMElement * operator()(const DOMElement &el) const;
        DOMElement * operator()(const DOMElement *el) const;
    private:
        std::string m_name;
    };

    struct FindFirstExisting
    {
        FindFirstExisting(const std::string& name);
        DOMElement & operator()(const DOMElement &el) const;
    private:
        std::string m_name;
    };

    inline DOMElement & operator|(const DOMElement &el, const FindFirstExisting &f)
    {
        return f(el);
    }

    inline DOMElement * operator|(const DOMElement &el, const FindFirst &f)
    {
        return f(el);
    }

    inline DOMElement * operator|(const DOMElement *el, const FindFirst &f)
    {
        return f(el);
    }

    inline FindFirst::FindFirst(const std::string & name)
        : m_name(name)
    {
    }

    inline DOMElement * FindFirst::operator()(const DOMElement &el) const
    {
        auto list = el.getElementsByTagName(_U16C(m_name));
        if (list->getLength() == 0)
            return nullptr;

        return static_cast<DOMElement *>(list->item(0));
    }

    inline DOMElement * FindFirst::operator()(const DOMElement *el) const
    {
        if (el == nullptr)
            return nullptr;

        auto list = el->getElementsByTagName(_U16C(m_name));
        if (list->getLength() == 0)
            return nullptr;

        return static_cast<DOMElement *>(list->item(0));
    }

    inline FindFirstExisting::FindFirstExisting(const std::string & name)
        : m_name(name)
    {
    }

    inline DOMElement & FindFirstExisting::operator()(const DOMElement & el) const
    {
        auto list = el.getElementsByTagName(_U16C(m_name));
        if (list->getLength() == 0)
            throw runtime_error(string("Missing element with name ") + m_name);

        return static_cast<DOMElement &>(*list->item(0));
    }

XERCES_CPP_NAMESPACE_END

Il peut être utilisé de cette façon:

auto packetRate = *elementRoot | FindFirst("Header") | FindFirst("PacketRate");
auto &decrypted = *elementRoot | FindFirstExisting("Header") | FindFirstExisting("Decrypted");
1
ceztko

Vous pouvez activer les méthodes d'extension kinda pour votre propre classe/structure ou pour un type spécifique dans une certaine portée. Voir la solution approximative ci-dessous.

class Extensible
{
public:
    template<class TRes, class T, class... Args>
    std::function<TRes(Args...)> operator|
        (std::function<TRes(T&, Args...)>& extension)
    {
        return [this, &extension](Args... args) -> TRes
        {
            return extension(*static_cast<T*>(this), std::forward<Args>(args)...);
        };
    }
};

Héritez ensuite de votre classe et utilisez comme

class SomeExtensible : public Extensible { /*...*/ };
std::function<int(SomeExtensible&, int)> fn;
SomeExtensible se;
int i = (se | fn)(4);

Ou vous pouvez déclarer cet opérateur dans un fichier cpp ou un espace de noms.

//for std::string, for example
template<class TRes, class... Args>
std::function<TRes(Args...)> operator|
    (std::string& s, std::function<TRes(std::string&, Args...)>& extension)
{
    return [&s, &extension](Args... args) -> TRes
    {
        return extension(s, std::forward<Args>(args)...);
    };
}

std::string s = "newStr";
std::function<std::string(std::string&)> init = [](std::string& s) {
    return s = "initialized";
};
(s | init)();

Ou même envelopper en macro (je sais, c'est généralement une mauvaise idée, néanmoins vous pouvez):

#define ENABLE_EXTENSIONS_FOR(x) \
template<class TRes, class... Args> \
std::function<TRes(Args...)> operator| (x s, std::function<TRes(x, Args...)>& extension) \
{ \
    return [&s, &extension](Args... args) -> TRes \
    { \
        return extension(s, std::forward<Args>(args)...); \
    }; \
}

ENABLE_EXTENSIONS_FOR(std::vector<int>&);
0
KindElk