many_cast : passer de std::vector<std::any> à une décomposition

Besoin de conseils et de suggestions (et de critiques !)

a marqué ce sujet comme résolu.

J’ai bien conscience que c’est un peu avancé comme interrogation et que l’habitude des choses voudrait que j’aille la poser sur un forum anglais, mais d’une part : je ne sais pas lequel ; d’autre part : j’aimerais bien qu’on stimule un peu plus la communauté C++ de zeste de savoir et francophone en général :)

Mon cas d’usage

J’ai une interface par fonction membre virtuelle que je redéfinis pour un cas concret (si ça vous intéresse, ça vient d’ici : https://gitlab.com/inendi/inspector/ ).

Pour tous les paramètres inconnus de la classe abstraite, il y a un std::vector<std::any>>.

using Params = std::vector<std::any>;
QWidget* PVDisplays::PVDisplayViewGroupBy::create_widget(Inendi::PVView* view,
                                                         QWidget* parent,
                                                         Params const& params) const
{
    auto col1 = std::any_cast<PVCol>(params.at(0));
    auto col2 = std::any_cast<PVCol>(params.at(1));
    auto operation = std::any_cast<Operation>(params.at(2));

    //...
}

Ce code me paraît redondant, sujet aux erreurs (notamment d’indice) et peu agréable à lire (il y a pire me direz-vous).

many_cast

J’ai donc essayé de simplifier l’usage avec un many_cast calqué sur std::any_cast.

J’ai essayé de le faire en une seule fonction, mais je n’y suis pas arrivé ; si vous avez une meilleure solution, je suis preneur !

template<class... Args, std::size_t... I>
auto mcimpl(auto& params, std::index_sequence<I...>)
{
	return std::make_tuple(std::any_cast<Args>(params.at(I))...);
}

template<class... Args, class Seq = std::make_index_sequence<sizeof...(Args)>>
auto many_cast(auto& params)
{
	return mcimpl<Args...>(params, Seq{});
}

Ce qui donne à l’usage :

using Params = std::vector<std::any>;
QWidget* PVDisplays::PVDisplayViewGroupBy::create_widget(Inendi::PVView* view,
                                                         QWidget* parent,
                                                         Params const& params) const
{
    auto [col1, col2, operation] = many_cast<PVCol, PVCol, Operation>(params);

    //...
}

Côté usage, ça me semble bien mieux : plus d’erreur possible dans les indices, et un one-liner.

Si d’aventure j’avais un nombre de paramètres dynamique, ils restent accessibles, ce n’est donc pas intrusif.

J’ai utilisé un std::tuple, mais comme on le voit dans l’utilisation, ça reste un détail d’implémentation. Si d’autres alternatives vous viennent, je suis intéressé.

Les exceptions

Là où ça se corse, c’est si je cherche à gérer les exceptions. J’hésite à faire une gestion différente entre many_cast et std::any_cast. Les deux types d’exceptions potentielles à gérer sont std::bad_any_cast et std::out_of_range. Il y a bien évidemment toutes les exceptions des constructeurs de copie, mais il me semble que cela n’a pas d’impact particulier (et je ne vois pas l’intérêt de les gérer).

Copie uniquement

Autre difficulté (qui n’en est pas une dans mon cas d’usage, mais pourrait l’être si je réutilise many_cast dans un autre contexte), many_cast fait systématiquement une copie des paramètres pour les extraire.

std::any_cast est plus subtil, il permet de récupérer par référence, ou par déplacement.

Je ne suis du tout confiant dans la robustesse de many_cast pour ce type d’accès, qui plus est potentiellement mixé : est-ce que le passage via std::make_tuple, qui est un détail d’implémentation, ne rend pas impossible de récupérer des références ?

Code générique

Pour jouer avec et préparer une généralisation :

#include <any>
#include <vector>
#include <tuple>
#include <string>

template<class... Args, std::size_t... I>
auto mcimpl(auto& params, std::index_sequence<I...>)
{
    return std::make_tuple(std::any_cast<Args>(params.at(I))...);
}

template<class... Args, class Seq = std::make_index_sequence<sizeof...(Args)>>
auto many_cast(auto& params)
{
    return mcimpl<Args...>(params, Seq{});
}

int main()
{
    std::vector<std::any> params{42, 3.14, std::string{"bar"}};

    auto [answer, pi, name] = many_cast<int, double, std::string>(params);
}

Hâte de lire vos réflexions :)

+0 -0

Pour moi,l’utilisation de .at() est à proscrire. Déjà, parce que permettre un dépassement ne devrait pas arriver et ensuite parce qu’une exception aussi générique que std::out_of_range n’apporte aucun contexte exploitable. Le seul endroit où c’est exploitable est au moment de l’appel, sauf qu’à ce moment, vérifier la taille est beaucoup moins verbeux qu’un système d’exception (en plus d’être bien mieux localisé d’un try/catch qui englobe un bloc plus large).

Du coup, la vérification devrait se faire dans many_cast avec un type d’exception propre. En général je fais 2 versions unsafe_foo()/foo() ou unsafe_foo()/foo() en fonction du besoin visé. S’il y a beaucoup d’option possible, j’ajoute un premier paramètre optionnel qui active / désactive certains comportements (comme on le ferrait avec une classe de trait ou de politique).

Pour std::bad_any_cast, il faut voir ce que tu veux en faire, mais si c’est pour remplacer un type incorrect par une valeur, je pense que le plus simple est de wrapper le paramètre dans une sorte de value_or<T, F> qui utiliserait F si std::any_cast<T>(any*) retourne nullptr.

Autre difficulté, many_cast fait systématiquement une copie des paramètres pour les extraire.

Pour éviter cela, il faudrait propager le type de référence à mcimpl et prendre des auto&& params. C’est aussi possible de faire comme avec ma solution value_or plus haut pour le faire paramètre par paramètre.

Concernant std::make_tuple, ne pas conserver les références est documenté. Le mieux est de directement faire std::tuple<Args...>. Mais ici, le type de retour de tous les any_cast est T, std::tuple utilisera toujours un déplacement pour construire ses valeurs.

Un code d’exemple value_or (j’ai aussi comment faire une seule fonction pour many_cast).

#include <any>
#include <vector>
#include <tuple>
#include <string>
#include <iostream>


template<class T>
struct mc_value
{
  template<class Any>
  decltype(auto) operator()(Any&& any)
  {
    return std::any_cast<T>(static_cast<Any&&>(any));
  }
};

template<class T, auto make>
struct mc_value_or
{
  T operator()(auto* any)
  {
    auto* p = std::any_cast<T>(any);
    return p ? *p : make();
  }

  T operator()(auto& any)
  {
    return operator()(&any);
  }
};

template<class T>
T make_default()
{
  return T();
}

template<class T>
using mc_value_or_default = mc_value_or<T, make_default<T>>;

template<class T>
struct to_mc_value
{
  using type = mc_value<T>;
};

template<class T, auto make>
struct to_mc_value<mc_value_or<T, make>>
{
  using type = mc_value_or<T, make>;
};

template<class T>
using to_mc_value_t = typename to_mc_value<T>::type;

template<class... Args>
auto many_cast(auto& params)
{
    return [&]<std::size_t... I>(std::index_sequence<I...>, auto... casts) {
        return std::tuple<decltype(casts(params[I]))...>{ casts(params[I])... };
    }(std::make_index_sequence<sizeof...(Args)>(), to_mc_value_t<Args>()...);
}

int main()
{
    std::vector<std::any> params{42, 3.14, std::string{"bar"}, std::any()};

    auto [answer, pi, name, x] = many_cast<int, double, std::string, mc_value_or_default<int>>(params);
    std::cout << answer << " " << pi << " " << name << " " << x << "\n";
}
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte