6
votes

Listes à coût nul pour les fonctions en ligne en C ++

J'aime écrire des chèques pour une fonction sur une liste. Pour cela, j'écris généralement une fonction comme celle-ci:

if (is_good("a") and is_good("b") and /* that's too much, man */ is_good("c")) {...}

Ensuite, je peux écrire comme if (all_good ({"a", "b", "c", "d", "e"})) {...} et ça a l'air vraiment sympa. Ceci est bon à utiliser lorsque votre chèque pour quelques éléments grossit comme ceci:

inline bool good_strings(const std::vector<const char *> & items)
{
    for (i in items) {
        if (not is_good(i)) return false;
    }
    return true;
}

Mais je suis préoccupé par la surcharge du conteneur que j'utilise, et il est également difficile d'en choisir un: std :: vector , std :: list , QList , QStringList ou peut-être même std :: array ou std :: initializer_list - qui devrait être utilisé pour les fonctions en ligne ? Et lequel de ces a frais généraux minimaux ou même nuls lors de la création en utilisant les crochets {} ?

D'accord, et mettre à jour: j'ai saisi mon ami sous licence IDA Pro et vérifié certaines options.

  • std :: initializer_list : la fonction n'est même pas en ligne, et là est une surcharge pour la création de la liste et la copie des pointeurs.
  • std :: vector : la fonction est en ligne, cependant, il y a un surcharge pour créer un vecteur et y copier des pointeurs.
  • std :: array : pas aussi beau en raison de la spécialisation des modèles, et la fonction n'est pas en ligne. Donc l'appeler plusieurs fois crée de nombreux morceaux de code similaires. Cependant, il n'y a pas de surcharge pour la baie création, et tous les pointeurs sont passés en tant que paramètres de fonction, qui est rapide pour le registre x86_64 appelant une convention.

La question demeure, existe-t-il un conteneur absolument nul ?


6 commentaires

Pourquoi ne pas modéliser toute la fonction? Ensuite, il peut utiliser n'importe quel conteneur que vous lui avez transmis.


La complexité des paramètres ne devrait pas affecter les fonctions inlining ou non. Vous passez ce paramètre par référence, c'est donc juste une adresse stockée dans un registre (pour la plupart des compilateurs). Le compilateur décide généralement d'insérer quelque chose sur le code qui apparaît à l'intérieur de la fonction, des prédictions de branche, etc. Vous ne devriez pas avoir à vous soucier beaucoup des types de paramètres.


Votre objectif est-il d'évaluer tous les résultats au moment de la compilation, ou tout simplement aussi efficacement que possible au moment de l'exécution?


std :: all_of (items.begin (), items.end (), is_good);


@JeremyFriesner la fonction is_good est sideeffectful et doit être évaluée à l'exécution, cependant ses paramètres sont toujours les mêmes et peuvent être passés au moment de la compilation.


Est-ce exclusivement pour un contexte constexpr ? Si vous pouvez effectuer ce test lors de la compilation, la surcharge n'a pas vraiment d'importance.


4 Réponses :


12
votes

Aucun des conteneurs n'entraînera aucun surcoût. std :: array ou std :: initializer_list vous donnera le moindre coût. std :: array a besoin de son type et de sa taille spécifiés au moment de la compilation, donc il est un peu moins convivial qu'un std :: initializer_list dans ce cas. Ainsi, utiliser un std :: initializer_list sera le "conteneur" le plus petit et le plus facile à utiliser que vous puissiez utiliser. Cela coûtera la taille du tableau de pointeurs généré par le compilateur et peut-être un peu plus et cela ne nécessitera aucune allocation de mémoire dynamique.


Si vous pouvez utiliser C ++ 17 Vous n'avez même pas besoin d'un récipient. Utilisation d'un modèle variadic et d'un expression de repli vous pouvez avoir tous les arguments passés à la fonction en tant que paramètres séparés et appliquer la même opération à tous les arguments.

return is_good("a") && is_good("b") && ... && is_good("e");

tournera

all_good("a", "b", "c", "d", "e")

into

template<typename... Args>
bool good_strings(Args&&... args)
{
    return (is_good(args) && ...);
}

qui exploite les courts-circuits afin qu'il cesse d'évaluer dès que le premier appel à is_good retourne false.

Vous pouvez utiliser le modèle variadic en C ++ 11, mais vous devrez soit utiliser la récursion, soit construire votre propre tableau, ce qui ne vous rapporte vraiment rien avec la complexité supplémentaire que vous auriez.


7 commentaires

Très agréable. Vous pouvez également faire de is_good un autre argument pour que le prédicat puisse être facilement modifié.


Oui, c'est une bonne solution, et c'est ce que j'aimerais utiliser, mais malheureusement, ma plate-forme cible n'a pas encore C ++ 14.


@MorJ Cela nécessite en fait C ++ 17 (pour l'expression fold). À quelle version de C ++ êtes-vous limité?


Eh bien, j'ai vérifié et vous vous trompez sur initializer_list. (-; Vérifiez ma réponse si vous êtes intéressé, et je peux envoyer mon fichier .idb avec le binaire de test commenté un peu.


Au fait, j'ai trouvé un petit problème avec les expressions de pliage: il n'y a aucun moyen d'écrire un pack de paramètres avec des types concrets. Et curieusement, la question de stackoverflow qui m'en a parlé disait que je devrais utiliser std :: vector pour le travail. ( stackoverflow.com / questions / 18017543 /… )


@MorJ pourquoi avez-vous besoin de types concrets dans votre pack de paramètres? Si vous transmettez quelque chose qui n'est pas acceptable à is_good , vous obtiendrez une erreur de compilation, et les compilateurs s'améliorent de plus en plus pour donner des messages utiles dans ces cas


@MorJ Avez-vous besoin de types de béton? Si vous voulez limiter les Args , vous pouvez utiliser SFINAE pour contraindre les types. Cela dit, vous devriez obtenir une erreur de compilation okay-ish si vous utilisez un objet que is_good ne prend pas en charge en vous indiquant qu'il vient de all_good .



-2
votes

Si vous modélisez la fonction ainsi:

bool is_good(const std::string &) { return true; )
template<typename Container>
inline bool good_strings(const Container & items)
{
    for (auto const &i : items){
        if (not is_good(i)) return false;
    }
    return true;
}

alors l'appel good_strings (std :: initializer_list {"a", "b", "c "," d "," e "}) passera la liste d'initialisation à all_good . Aucun conteneur nécessaire.


3 commentaires

La compilation échoue car {...} n'a pas de type: coliru.stacked-crooked.com/a/54814bd7767b79c7


Container est-il lié à des concepts? OP a ajouté une balise pour c ++ 11


J'ai essayé cela et cela ne compile pas dans gcc pour c ++ 11. Apparemment, le type de modèle ne peut pas être déduit au moment de la compilation pour ce type de littéral de liste polymorphe. Oh comme Haskell me manque.



0
votes

D'accord, grâce aux idées des réponses précédentes, j'ai compris. Si ce que les gens disent à propos de "il n'y a pas de meilleurs conteneurs", alors std :: array est le meilleur. Lorsqu'il est compilé avec le niveau d'optimisation -O2 , std :: array n'a ni copie de paramètre, ni appel de fonction, tandis que std :: initializer_list a une copie de paramètre. Une fois compilé avec -O0 , tout est comme je l'ai décrit dans la question elle-même.

Donc ma solution: utilisez std :: array et faites face à la spécification de pour le nombre d'arguments.


2 commentaires

Passer la liste d'initialisation par référence ( const & ) peut améliorer ses performances.


@NathanOliver Passer par référence const est implicite, il n'y aurait aucun espoir d'optimisation sans elle bien sûr.



-1
votes

Si vous êtes vraiment préoccupé par l'utilisation d'un conteneur, vous pouvez simplement écrire N surcharges, par exemple pour N = 5 :

inline bool good_string(const char* a)
{
    return true;
}
inline bool good_strings(const char* a, const char* b)
{
    return good_string(a) && good_string(b);
}
inline bool good_strings(const char* a, const char* b, const char* c)
{
    return good_strings(a, b) && good_string(c);
}
inline bool good_strings(const char* a, const char* b, const char* c, const char* d)
{
    return good_strings(a, b, c) && good_string(d);
}
inline bool good_strings(const char* a, const char* b, const char* c, const char* d, const char* e)
{
    return good_strings(a ,b, c, d) && good_string(e);
}


4 commentaires

Je suis désolé, mais votre solution est horrible: je veux éviter la répétition, et la répétition est ce que vous suggérez. Votre solution fait un gros problème d'un petit problème. D'un autre côté, on pourrait améliorer ce fonctionnement et l'écrire avec des packs de paramètres et la récursivité. Cependant, comme l'a dit une autre question stackoverflow que je viens de trouver, il n'y a aucun moyen d'écrire un pack de paramètres avec des types concrets.


Je pense que vous comprenez mal le concept de répétition. Avec ce code, vous ne vous répétez pas, vous créez des surcharges où chacune des surcharges a son propre but! Consultez StrCat sur abseil.io ici . Ils appliquent le même concept là-bas.


D'accord, j'ai des opinions bien arrêtées sur la répétition, et ce n'est pas le lieu de les promettre. Mais merci, j'ai vérifié la source du rappel et cela ressemble à une poubelle. Ce qui me déroute, c'est que les auteurs connaissent les packs de paramètres, mais ne les utilisent qu'après 5 arguments. De plus, ils aiment vraiment la magie du pointeur, reinterpret_cast et void *, qui sont également de mauvais signes pour un projet.


Comme je l'ai dit, ce que j'ai publié n'a rien à voir avec la répétition.