Journal Conversion entre pointeurs de fonctions incompatibles

Posté par  (site web personnel) . Licence CC By‑SA.
Étiquettes :
14
7
nov.
2018

Posons nous dans le cas suivant (oui ça commence direct)

int strange_apply(int (*f)(int) {

    return reinterpret_cast<(int(*)(int, int)>(f)(1, 2);

}

Ce code compile avec un warning depuis le dernier gcc, et c'est bien car c'est en fait un undefined behavior (cast entre types de fonctions incompatibles dans le cas présent).

Et pourtant on voit pas mal de code comme ça, p.e. dans les sources de LLVM, sous l'hypothèse que si on utilise pas les derniers arguments, ce n'est pas grave.

Je me suis amusé à créer un nouveau function_cast qui permet de juste passer les valeurs dont on a besoin pour éviter cet avertissement:

#include <utility>
#include <tuple>


template<typename To, typename From>
class FunctionCast;


template<typename ReturnType, typename... ArgumentTypes, typename FromReturnType, typename... FromArgumentTypes>
class FunctionCast<ReturnType(ArgumentTypes...), FromReturnType(*)(FromArgumentTypes...)> {

    using From = FromReturnType (*)(FromArgumentTypes...);

    From from;

    template<typename Args, std::size_t... Is>
    ReturnType apply(Args args, std::index_sequence<Is...>) const {
        return from(std::get<Is>(args)...);
    }

    public:
    FunctionCast(From f) : from(f) {}
    ReturnType operator()(ArgumentTypes... args) const {
        return apply(std::make_tuple(args...), std::make_index_sequence<sizeof...(FromArgumentTypes)>());
    }


};

template<typename To, typename From>
FunctionCast<To, From> function_cast(From f) {
    return {f};
}


int apply(int (*f)(int)) {
    return reinterpret_cast<int(*)(int, int)>(f)(1, 2); // invalid
    return function_cast<int(int, int)>(f)(1, 2);       // valid
}

lien godbolt illustrant l'idée. Le concept est assez simple : on empaquète les arguments dans un tuple, puis on applique la fonction en dépaquetant uniquement une partie des arguments (au passage, on doit perdre quelques informations de type, std::make_tuple n'est pas très conservateur à ce sujet).

Et voilà, un nouveau cast qui, malheureusement, ne permet pas de créer de nouveaux pointeurs de fonctions mais un foncteur…

  • # std::function

    Posté par  (site web personnel) . Évalué à 1.

    Et sinon, une raison particulière pour ne pas utiliser std::function ?

    git is great because linus did it, mercurial is better because he didn't

    • [^] # Re: std::function

      Posté par  (site web personnel) . Évalué à 3.

      Car ça ne résout pas le problème

      int strange_apply(int (*f)(int) )
      {
          std::function<int(int, int)> f1 = f; // Ne compile pas
          std::function<int(int)> f2 = f; 
          f2(1, 2); // ne compile pas.
      }
      • [^] # Re: std::function

        Posté par  (site web personnel) . Évalué à 2. Dernière modification le 08 novembre 2018 à 14:34.

        Je ne comprends pas ton problème. Ton contrat c'est de prendre des fonctions qui prennent deux entiers en paramètre. Sauf qu'on t'envoie une fonction qui en prend que un. C'est contraire au contrat indiqué. C'est à l'appelant de se modifier.

        Sinon, c'est tout à fait possible avec std::function et une lambda (avec std::bind peut-être)

        int strange_apply(int (*beurk)(int))
        {
            std::function<int(int, int)> f = [beurk] (int x, int) -> int {
                return beurk(x);
            };
        
            f(123, 456);
        }

        Note que le std::function n'est nécessaire que si tu souhaite stocker beurk pour l'appeler plus tard.

        git is great because linus did it, mercurial is better because he didn't

        • [^] # Re: std::function

          Posté par  (site web personnel) . Évalué à 2.

          On peut imaginer un système d'enregistrement de plugin avec des paramètres optionnels (je dis pas que c'est le bon design hein) :

          void some_plugin();
          
          template<class F>
          void register_(F plugin) {
               plugin(1/*verbosity*/);
          }
          
          
          int main() {
              register_(function_cast<void(int)>(some_plugin));
              return 0;
          }

          Ça permet d'avoir des paramètres optionnels pour les plugins.

  • # Mensonges !

    Posté par  (site web personnel) . Évalué à 2.

    Ce code compile

    Étant donné qu'il manque deux parenthèses fermantes, je doute que ça compile.

    Ensuite, concernant LLVM et le "undefined behaviour", ben, vu que LLVM est probablement un compilo qui bootstrappe, ils font ce qui les arrange avec les "undefined behaviours"…

  • # Imbitable

    Posté par  . Évalué à 10.

    Langage imbitable

  • # Crados

    Posté par  (site web personnel) . Évalué à 7.

    Et pourtant on voit pas mal de code comme ça, p.e. dans les sources de LLVM, sous l'hypothèse que si on utilise pas les derniers arguments, ce n'est pas grave.

    Ben c'est crade. Si tu as besoin de ça tu crée une fonction intermédiaire propre qui n'utilise pas le dernier argument et tu l'inline pour les perfs. Mais jouer comme ça, non.

    Python 3 - Apprendre à programmer dans l'écosystème Python → https://www.dunod.com/EAN/9782100809141

    • [^] # Re: Crados

      Posté par  (Mastodon) . Évalué à 4.

      Du coup, c'est exactement ce que fait function_cast mais de manière totalement générique.

  • # extern "C", pointer to member function,

    Posté par  (site web personnel) . Évalué à 3.

    Une autre source de undefined behaviour est quand on a des pointeur extern "C"

    Par exemple:

    extern "C" int my_function(int); // Une fonction d'une bibliothèque C
    
    int foo() {
        int (*f)(int) = my_function; // Compile sans warnings
        f(42); // undefined behaviour
    
        // undefined behaviour dans function_cast::operator()
        return apply(my_function);
    }

    Il serrait bien aussi de trouver une solution qui pour créé une fonction générique qui fonctionne avec les pointer vers des fonction membre.

    struct Struct {
        int foo(int) const;
    };
    
    template<typename T> int my_apply(int(T::*f)(int)) {
        return (T().*f)(42);
    }
    
    int call_with_foo() {
        //my_apply(&Struct::foo); // ah zut, ça compile pas, à cause du const
    
        // Je peux essayer avec un cast, mais alors "undefined behavior"
        return my_apply(reinterpret_cast<int(Struct::*)(int)>(&Struct::foo)); 
    };

    Il faut que je fasse un overload pour my_apply(int(T::*f)(int) const).

    Mais si je quelqu'un veut passer une fonction volatile ? Ah oui, il faut rajouter my_apply(int(T::*f)(int) volatile) et my_apply(int(T::*f)(int) const volatile)

    Bon, en C++98 c'est bon, mais en C++11 il faut aussi considérer les "Ref-qualifiers".
    Rajoutons donc des overload. my_apply(int(T::*f)(int) const&) et my_apply(int(T::*f)(int) &&). Je vais passer tous les équivalent avec volatile car bon, qui utilise volatile? mais je rajoute quand même my_apply(int(T::*f)(int) const&&) pour être sur.

    Et quand on crois que on a fini, C++17 fait un changement incompatible et le code code ne compile plus si quelqu'un passe une fonction noexcept alors il faut que je dédouble le tout.

    Au final je dois écrire 4 * 3 * 2 = 24 overloads.

  • # Pourquoi?

    Posté par  (site web personnel) . Évalué à 8.

    Quel problème cherches-tu à résoudre?

    Le post ci-dessus est une grosse connerie, ne le lisez pas sérieusement.

    • [^] # Re: Pourquoi?

      Posté par  . Évalué à 8.

      L'excès de popularité du C++, mais vu la gueule du code, je crois que c'est réglé.

      ---(Je sais, j'aurais dû rester dehors…)---> [ ]

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.