1
votes

Est-il possible de transférer des appels de fonction avec le type de modèle correspondant pour un conteneur std :: any-like?

Je n'ai pas trouvé de moyen de réaliser ce que je veux, mais je ne suis pas suffisamment informé pour savoir si c'est impossible. Une aide serait appréciée.

Le principal conteneur de données de notre logiciel se comporte un peu comme un std :: variant ou std :: any: il a une classe de base BaseContainer qui fournit une énumération de type. L'instance dérivée DataContainer contient les données réelles dans une variable membre tenseur typée. Ainsi, un exemple simplifié se résume à quelque chose comme ceci:

void processData(BaseContainer* aContainer) {
    switch (vContainer->getType()) {
        case DataTypes::INT8:
            return processData(dynamic_cast<DataContainer<int8_t>>(vContainer)->getData());
        case DataTypes::UINT8:
            return processData(dynamic_cast<DataContainer<uint8_t>>(vContainer)->getData());
        case DataTypes::INT16:
            return processData(dynamic_cast<DataContainer<int16_t>>(vContainer)->getData());
        case DataTypes::UINT16:
            return processData(dynamic_cast<DataContainer<uint16_t>>(vContainer)->getData());
...
        default:
            throw(std::runtime_error("Type not supported"));
    }
}

Nous avons de nombreuses méthodes qui traitent les données en fonction du type et des dimensions du modèle sous-jacent:

template<typename T>
void processData(const tensor<T>& aTensor, ...other arguments...);

Le problème est que pour chaque méthode comme processData () que nous voulons appeler avec un BaseContainer , nous devons écrire une méthode de liaison qui démêle les types possibles pour appeler la version typée de processData():

BaseContainer* vContainer = new DataContainer<float>({1000000});
if (vContainer->getType() == DataTypes::FLOAT)
    const Tensor<float>& vTensor = dynamic_cast<DataContainer<float>>(vContainer)->getData();

Ma question est: est-il possible de faire une seule méthode "adaptateur" (dans n'importe quelle version publiée de c ++) qui peut prendre une fonction (comme processData () ), un BaseContainer et potentiellement une liste d'arguments, et appeler la liaison de modèle correcte de cette fonction avec les arguments?

Je n'ai pas réussi à lier dynamiquement une fonction de modèle car je ne pouvais pas passer le nom sans le type de modèle. Pourtant, le type de modèle devrait être dynamique basé sur le BaseContainer. Mais peut-être existe-t-il d'autres moyens pour réaliser ce que je veux faire? Je suis très curieux de connaître toute solution, surtout pour étendre ma compréhension, tant que la complexité de la solution est inférieure à l'écriture de centaines de méthodes d'adaptateur.

Si rien d'autre, serait-il possible de générer le méthodes "adapter" utilisant des macros de préprocesseur?


2 commentaires

std :: variant a std :: visit . Débarrassez-vous de la solution locale et utilisez std :: variant , ou écrivez votre propre version de std :: visit .


Si vous faites dynamic_cast , vous avez des méthodes virtuelles. Peut-être qu'un modèle de visiteur pourrait être utilisé ici?


3 Réponses :


1
votes

Si vous souhaitez écrire une petite classe wrapper pour chaque fonction de type processData , vous pouvez faire quelque chose comme ceci:

// One like this for each function.
struct ProcessDataWrapper {
  template <typename... Args>
  static auto run(Args&&... args) {
    return processData(std::forward<Args>(args)...);
  }
};

template <typename Wrapper>
auto ProcessGeneric(BaseContainer* aContainer) {
    switch (vContainer->getType()) {
        case DataTypes::INT8:
            return Wrapper::run(dynamic_cast<DataContainer<int8_t>>(vContainer)->getData());
    // ...
}

// Called as
ProcessGeneric<ProcessDataWrapper>(myContainer);


0 commentaires

1
votes

C'est possible, mais comme le disent les commentaires, cela pourrait valoir la peine de considérer std :: visit.

Voici une solution nécessitant C ++ 17 qui ne nécessite que deux lignes pour chaque modèle de fonction que vous voulez envelopper. Vous pouvez utiliser une simple macro pour simplifier davantage l'écriture.

L'idée de base est d'avoir une fonction cast qui mappe d'une énumération DataType à la correspondant à DataContainer , puis pour tirer parti des expressions c ++ 17 fold pour envelopper l'instruction switch dans votre code.

Voici la fonction cast , nous avons donc exactement un endroit pour mapper de DataType au DataContainer actuel :

    ([&]{ if(c.GetDataType() == types ) {  std::invoke(f, cast<types>(c), args...); return true; } return false; }() || ...  || []() -> bool{ throw (std::runtime_error("Type not supported")); }());
                                                                                                                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

C'est plutôt une aide pratique pour faire le code suivant un peu plus lisible. Le bloc de code suivant utilise l'expression c ++ 17 fold pour distribuer la fonction en fonction du type du conteneur.

int main() {

    DataContainer<float> f;
    DataContainer<int> i;
    pd(f, 2); // prints float, 2
    pd(i, 4); // prints int, 4
}

L'idée de base est d'envelopper la fonction dans un lambda qui vérifie le DataType du DataContainer et n'appelle la fonction que si elle correspond. L'expression Fold sur l'opérateur || est utilisée pour décompresser les DataTypes.

Exemple d'utilisation:

template<typename T>
void processData(DataContainer<T>& c, int arg) {
    if constexpr(std::is_same_v<T, int>) std::cout << "int";
    else if constexpr(std::is_same_v<T, float>) std::cout << "float";
    std::cout << ", arg: " << arg << '\n';
}


0 commentaires

1
votes

Vous ne pouvez pas passer les surcharges par nom, mais vous pouvez passer fonctor avec operator () surchargé comme lambda générique.

Donc

dispatch(vContainer, [](auto* data){ return processData(data); });

avec utilisation

template <typename F>
auto dispatch(BaseContainer& vContainer, F f) {
    switch (vContainer.getType()) {
        case DataTypes::INT8:
            return f(dynamic_cast<DataContainer<int8_t>&>(vContainer).getData());
        case DataTypes::UINT8:
            return f(dynamic_cast<DataContainer<uint8_t>&>(vContainer).getData());
        case DataTypes::INT16:
            return f(dynamic_cast<DataContainer<int16_t>&>(vContainer).getData());
        case DataTypes::UINT16:
            return f(dynamic_cast<DataContainer<uint16_t>&>(vContainer).getData());
...
        default:
            throw (std::runtime_error("Type not supported"));
    }
}


1 commentaires

Il existe maintenant plusieurs bonnes réponses, la plupart basées sur le modèle des visiteurs. Mais j'ai choisi cela comme réponse acceptée car cela donne une très bonne flexibilité et une intégration facile dans le code existant, par rapport aux fonctions qui ont plusieurs arguments. Le lambda peut lier par défaut la plupart des arguments. Seuls les conteneurs réels BaseContainer doivent être extraits via dispatch () . Cela permet de garder le code court et lisible. Il était également facile d'ajouter plus de variantes de dispatch () qui prennent deux conteneurs ou plus, en fonction de la récursivité: pas besoin de spécifier le produit scalaire des types possibles dans le code.