Question

Il s'agit d'un sondage d'opinions sur la manière la plus lisible de faire quelque chose : qu'il s'agisse d'utiliser un pointeur C++ vers un membre, un décalage d'octets ou un foncteur modélisé pour définir "sélectionner le membre X à partir de la structure foo".

J'ai un type qui contient un grand vecteur de structures et j'écris une fonction utilitaire qui fonctionne essentiellement comme un réduire sur une certaine gamme d’entre eux.Chaque structure associe un groupe de variables dépendantes à un point le long d'une dimension indépendante -- pour inventer un exemple simplifié, imaginez que cela enregistre une série de conditions environnementales pour une pièce au fil du temps :

// all examples are psuedocode for brevity
struct TricorderReadings
{
  float time;  // independent variable

  float tempurature;
  float lightlevel;
  float windspeed; 
  // etc for about twenty other kinds of data...
}

Ma fonction effectue simplement un interpolation cubique pour deviner ces conditions à un moment donné entre les échantillons disponibles.

// performs Hermite interpolation between the four samples closest to given time
float TempuratureAtTime( float time, sorted_vector<TricorderReadings> &data)
{
    // assume all the proper bounds checking, etc. is in place
    int idx = FindClosestSampleBefore( time, data );
    return CubicInterp( time, 
                        data[idx-1].time, data[idx-1].tempurature,
                        data[idx+0].time, data[idx+0].tempurature,
                        data[idx+1].time, data[idx+1].tempurature,
                        data[idx+2].time, data[idx+2].tempurature );
}

J'aimerais généraliser cette fonction afin qu'elle puisse être appliquée de manière générique à n'importe quel membre, pas seulement à la température.Je peux penser à trois façons de procéder, et même si elles sont toutes simples à coder, je ne suis pas sûr de savoir laquelle sera la plus lisible pour quiconque devra l'utiliser dans un an.Voici ce que j'envisage :


Syntaxe du pointeur vers le membre

typedef int TricorderReadings::* selector;
float ReadingAtTime( time, svec<TricorderReadings> &data, selector whichmember )
{
   int idx = FindClosestSampleBefore( time, data );
   return CubicInterp( time, data[idx-1].time, data[idx-1].*whichmember, 
                       /* ...etc */  );
}
// called like:
ReadingAtTime( 12.6f, data, &TricorderReadings::windspeed );

Cela semble être la manière la plus "C++" de le faire, mais cela semble étrange, et toute la syntaxe du pointeur vers le membre est rarement utilisée et donc mal comprise par la plupart des membres de mon équipe.C'est la méthode techniquement « correcte », mais aussi celle sur laquelle je recevrai les e-mails les plus confus.

Décalage de structure

float ReadingAtTime( time, svec<TricorderReadings> &data, int memberoffset )
{
   int idx = FindClosestSampleBefore( time, data );
   return CubicInterp( time, 
                       data[idx-1].time, 
                       *(float *) ( ((char *)(&data[idx-1]))+memberoffset ), 
                       /* ...etc */  );
}
// called like:
ReadingAtTime( 12.6f, data, offsetof(TricorderReadings, windspeed) );

Ceci est fonctionnellement identique à ce qui précède, mais effectue explicitement le calcul du pointeur.Cette approche sera immédiatement familière et compréhensible pour tous les membres de mon équipe (qui ont tous appris le C avant le C++), et elle est robuste, mais elle semble tout simplement dégueulasse.

Foncteur modélisé

template <class F>
float ReadingAtTime( time, svec<TricorderReadings> &data )
{
   int idx = FindClosestSampleBefore( time, data );
   return CubicInterp( time, 
                       data[idx-1].time, 
                       F::Get(data[idx-1]) ), 
                       /* ...etc */  );
}

// called with:
class WindSelector
{ 
   inline static float Get(const TricorderReadings &d) { return d.windspeed; }
}
ReadingAtTime<WindSelector>( 12.6f, data );

C'est la manière la plus simple et la plus STL de faire les choses, mais cela ressemble à tout un tas de typage et de syntaxe supplémentaires et de définitions de classe improvisées.Il compile presque exactement la même chose que les deux ci-dessus, mais il supprime également un tas de définitions de fonctions redondantes dans tout l'exécutable.(je l'ai vérifié avec /FAcs, mais peut-être que l'éditeur de liens les supprime à nouveau.)


Les trois ci-dessus fonctionneront et le compilateur émettra à peu près le même code pour chacun d’eux ;donc le choix le plus important que je dois faire est simplement ce qui est le plus lisible.Qu'en penses-tu?

Était-ce utile?

La solution

Si votre équipe est composée de gens raisonnablement intelligents, je dirais de leur faire confiance et leurs capacités, et utiliser la solution technique préférée que la syntaxe pointeur à membre offres. C'est le genre de chose qu'il a été fait pour.

Si vous êtes vraiment inquiet, vous pouvez prendre des mesures pour soulager des problèmes futurs par

  • Prenant note dans un commentaire près du typedef et l'usage que ce qu'on appelle la syntaxe « pointeur vers un membre », de sorte que les autres membres de l'équipe savent ce qu'il faut en
  • Pointez explicitement dans une revue de code, où beaucoup d'entre eux devraient être présents. Offre de changer si elle est considérée comme incompréhensible ou trop obscur pour l'entretien.

Les deux autres approches ont des problèmes, à la fois comme vous l'avez, et au-delà:

  • Les deux nécessitent plus de code, ont plus d'espace pour les fautes de frappe, etc.
  • La offsetof primitive est limitée dans ce type, il peut être appliqué à:

    En raison de la fonctionnalité étendue de struct en C ++, dans cette langue, l'utilisation de offsetof est limitée aux « types POD », qui pour les classes, correspond plus ou moins au concept C de struct (bien que les classes non dérivées avec que les fonctions publiques non membres virtuels et sans constructeur et / ou destructor serait également considéré comme POD).

De .

Autres conseils

Je trouve le Functor très clair sans canevas dans ce cas.

ReadingAtTime<WindSelector>( 12.6f, data );

A plus STL-ish chemin serait un foncteur générique qui rend l'accès via un pointeur à membre ressemble à un appel de fonction. Il pourrait ressembler à ceci:

#include <functional>

template <class T, class Result>
class member_pointer_t: public std::unary_function<T, Result>
{
    Result T::*member;
public:
    member_pointer_t(Result T::*m): member(m) {}
    Result operator()(const T& o) const { return o.*member; }
};

template <class T, class Result>
member_pointer_t<T, Result> member_pointer(Result T::*member)
{
    return member_pointer_t<T, Result>(member);
}

float ReadingAtTime( float time, const std::vector<TricorderReadings> &data, member_pointer_t<TricorderReadings, float> f )
{
   int idx = FindClosestSampleBefore( time, data );
   return CubicInterp( time, data[idx-1].time, f(data[idx-1]));
}

ReadingAtTime( 12.6f, data, &TricorderReadings::windspeed);

L'exemple comprend également une fonction d'assistance pour aider à déduire des arguments de modèle pour le foncteur (non utilisé dans cet exemple).

La ReadingAtTime fonction peut également accepter un foncteur basé sur un modèle:

template <class Func>
float ReadingAtTime( float time, const std::vector<TricorderReadings>& data, Func f);

ReadingAtTime( 12.6f, data, member_pointer(&TricorderReadings::windspeed));

De cette façon, vous pouvez utiliser toutes sortes de fonctions / foncteurs pour obtenir une valeur à partir des données [IDX - 1]., Pas seulement des pointeurs à membres

équivalents génériques de member_pointer pourraient être std :: tr1 :: bind ou std :: tr1 :: mem_fn.

Pour des choses simples, je préférerais la solution Pointer-to-member.Cependant, l’approche fonctrice présente deux avantages possibles :

  1. La séparation de l'algorithme des données vous permet d'utiliser l'algorithme pour plus de choses à l'avenir, sine il fonctionne avec tout ce qui vous permet de construire un bon fonctor.

  2. En ce qui concerne le n ° 1, cela pourrait faciliter les tests de l'algorithme, car vous avez un moyen de fournir des données de test à la fonction qui n'implique pas la création des objets de données complets que vous avez l'intention d'utiliser.Vous pouvez utiliser des objets simulés plus simples.

Cependant, je pense que l'approche fonctrice n'en vaut la peine que si la fonction que vous créez est très complexe et/ou utilisée dans de nombreux endroits différents.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top