J'ai besoin d'implémenter un grand nombre de classes dérivées avec différentes données de membre const. Le traitement des données doit être géré dans la classe de base, mais je ne trouve pas un moyen élégant d'accéder aux données dérivées. Le code ci-dessous fonctionne, mais je ne l'aime vraiment pas.
Le code doit s'exécuter dans un petit environnement intégré, donc une utilisation extensive du tas ou des bibliothèques fantaisie comme Boost n'est pas une option.
class Base
{
public:
struct SomeInfo
{
const char *name;
const f32_t value;
};
void iterateInfo()
{
// I would love to just write
// for(const auto& info : c_myInfo) {...}
u8_t len = 0;
const auto *returnedInfo = getDerivedInfo(len);
for (int i = 0; i < len; i++)
{
DPRINTF("Name: %s - Value: %f \n", returnedInfo[i].name, returnedInfo[i].value);
}
}
virtual const SomeInfo* getDerivedInfo(u8_t &length) = 0;
};
class DerivedA : public Base
{
public:
const SomeInfo c_myInfo[2] { {"NameA1", 1.1f}, {"NameA2", 1.2f} };
virtual const SomeInfo* getDerivedInfo(u8_t &length) override
{
// Duplicated code in every derived implementation....
length = sizeof(c_myInfo) / sizeof(c_myInfo[0]);
return c_myInfo;
}
};
class DerivedB : public Base
{
public:
const SomeInfo c_myInfo[3] { {"NameB1", 2.1f}, {"NameB2", 2.2f}, {"NameB2", 2.3f} };
virtual const SomeInfo *getDerivedInfo(u8_t &length) override
{
// Duplicated code in every derived implementation....
length = sizeof(c_myInfo) / sizeof(c_myInfo[0]);
return c_myInfo;
}
};
DerivedA instanceA;
DerivedB instanceB;
instanceA.iterateInfo();
instanceB.iterateInfo();
Vous n'avez pas besoin de virtuels ou de modèles ici. Ajoutez simplement un pointeur SomeInfo*
Et sa longueur à Base
, et fournissez un constructeur protégé pour les initialiser (et puisqu'il n'y a pas de constructeur par défaut, il ne sera pas possible d'oublier de les initialiser).
Le constructeur protégé n'est pas une exigence stricte, mais comme Base
n'est plus une classe de base abstraite, la protection du constructeur empêche Base
d'être instancié.
class Base
{
public:
struct SomeInfo
{
const char *name;
const f32_t value;
};
void iterateInfo()
{
for (int i = 0; i < c_info_len; ++i) {
DPRINTF("Name: %s - Value: %f \n", c_info[i].name,
c_info[i].value);
}
}
protected:
explicit Base(const SomeInfo* info, int len) noexcept
: c_info(info)
, c_info_len(len)
{ }
private:
const SomeInfo* c_info;
int c_info_len;
};
class DerivedA : public Base
{
public:
DerivedA() noexcept
: Base(c_myInfo, sizeof(c_myInfo) / sizeof(c_myInfo[0]))
{ }
private:
const SomeInfo c_myInfo[2] { {"NameA1", 1.1f}, {"NameA2", 1.2f} };
};
class DerivedB : public Base
{
public:
DerivedB() noexcept
: Base(c_myInfo, sizeof(c_myInfo) / sizeof(c_myInfo[0]))
{ }
private:
const SomeInfo c_myInfo[3] {
{"NameB1", 2.1f},
{"NameB2", 2.2f},
{"NameB2", 2.3f}
};
};
Vous pouvez bien sûr utiliser une petite classe d'adaptateur/wrapper sans frais généraux au lieu des membres c_info
Et c_info_len
Afin de fournir un accès plus agréable et plus sûr (comme begin()
et end()
support), mais cela sort du cadre de cette réponse.
Comme Peter Cordes l'a souligné, un problème avec cette approche est que les objets dérivés sont désormais plus grands de la taille d'un pointeur plus la taille d'un int
si votre code final utilise toujours des virtuels (fonctions virtuelles que vous n'avez pas montré dans votre message.) S'il n'y a plus de virtuel, alors la taille de l'objet ne fera qu'augmenter d'un int
. Vous avez dit que vous étiez dans un petit environnement intégré, donc si beaucoup de ces objets vont être vivants en même temps, alors cela pourrait vous inquiéter.
Peter a également souligné que puisque vos tableaux c_myInfo
Sont const
et utilisent des initialiseurs constants, vous pourriez aussi bien les faire static
. Cela réduira la taille de chaque objet dérivé de la taille du tableau.
Vous pouvez créer Base
un modèle et prendre la longueur de votre tableau const. Quelque chose comme ça:
template<std::size_t Length>
class Base
{
public:
struct SomeInfo
{
const char *name;
const float value;
};
const SomeInfo c_myInfo[Length];
void iterateInfo()
{
//I would love to just write
for(const auto& info : c_myInfo) {
// work with info
}
}
};
Et puis initialisez le tableau en conséquence à partir de chaque classe de base:
class DerivedA : public Base<2>
{
public:
DerivedA() : Base<2>{ SomeInfo{"NameA1", 1.1f}, {"NameA2", 1.2f} } {}
};
class DerivedB : public Base<3>
{
public:
DerivedB() : Base<3>{ SomeInfo{"NameB1", 2.1f}, {"NameB2", 2.2f}, {"NameB2", 2.3f} } {}
};
Et puis utilisez comme vous le feriez normalement. Cette méthode supprime le polymorphisme et n'utilise aucune allocation de tas (par exemple, pas de std::vector
), tout comme l'utilisateur SirNobbyNobbs demandé.
Bon alors simplifions toutes les complications inutiles :)
Votre code se résume vraiment à ce qui suit:
SomeInfo.h
struct SomeInfo
{
const char *name;
const f32_t value;
};
void processData(const SomeInfo* c_myInfo, u8_t len);
SomeInfo.cpp
#include "SomeInfo.h"
void processData(const SomeInfo* c_myInfo, u8_t len)
{
for (u8_t i = 0; i < len; i++)
{
DPRINTF("Name: %s - Value: %f \n", c_myInfo[i].name, c_myInfo[i].value);
}
}
data.h
#include "SomeInfo.h"
struct A
{
const SomeInfo info[2] { {"NameA1", 1.1f}, {"NameA2", 1.2f} };
static const u8_t len = 2;
};
struct B
{
const SomeInfo info[3] { {"NameB1", 2.1f}, {"NameB2", 2.2f}, {"NameB2", 2.3f} };
static const u8_t len = 3;
};
main.cpp
#include "data.h"
int
main()
{
A a;
B b;
processData(a.info, A::len);
processData(b.info, B::len);
}
Vous pouvez utiliser CRTP:
template<class Derived>
class impl_getDerivedInfo
:public Base
{
virtual const SomeInfo *getDerivedInfo(u8_t &length) override
{
//Duplicated code in every derived implementation....
auto& self = static_cast<Derived&>(*this);
length = sizeof(self.c_myInfo) / sizeof(self.c_myInfo[0]);
return self.c_myInfo;
}
};
class DerivedA : public impl_getDerivedInfo<DerivedA>
{
public:
const SomeInfo c_myInfo[2] { {"NameA1", 1.1f}, {"NameA2", 1.2f} };
};
class DerivedB : public impl_getDerivedInfo<DerivedB>
{
public:
const SomeInfo c_myInfo[3] { {"NameB1", 2.1f}, {"NameB2", 2.2f}, {"NameB2", 2.3f} };
};
Commencez avec un type de vocabulaire:
template<class T>
struct span {
T* b = nullptr;
T* e = nullptr;
// these all do something reasonable:
span()=default;
span(span const&)=default;
span& operator=(span const&)=default;
// pair of pointers, or pointer and length:
span( T* s, T* f ):b(s), e(f) {}
span( T* s, size_t l ):span(s, s+l) {}
// construct from an array of known length:
template<size_t N>
span( T(&arr)[N] ):span(arr, N) {}
// Pointers are iterators:
T* begin() const { return b; }
T* end() const { return e; }
// extended container-like utility functions:
T* data() const { return begin(); }
size_t size() const { return end()-begin(); }
bool empty() const { return size()==0; }
T& front() const { return *begin(); }
T& back() const { return *(end()-1); }
};
// This is just here for the other array ctor,
// a span of const int can be constructed from
// an array of non-const int.
template<class T>
struct span<T const> {
T const* b = nullptr;
T const* e = nullptr;
span( T const* s, T const* f ):b(s), e(f) {}
span( T const* s, size_t l ):span(s, s+l) {}
template<size_t N>
span( T const(&arr)[N] ):span(arr, N) {}
template<size_t N>
span( T(&arr)[N] ):span(arr, N) {}
T const* begin() const { return b; }
T const* end() const { return e; }
size_t size() const { return end()-begin(); }
bool empty() const { return size()==0; }
T const& front() const { return *begin(); }
T const& back() const { return *(end()-1); }
};
ce type a été introduit en C++ std
(avec de légères différences) via le GSL. Le type de vocabulaire de base ci-dessus suffit si vous ne l'avez pas déjà.
Une plage représente un "pointeur" vers un bloc d'objets contigus de longueur connue.
Maintenant, nous pouvons parler d'un span<char>
:
class Base
{
public:
void iterateInfo()
{
for(const auto& info : c_mySpan) {
DPRINTF("Name: %s - Value: %f \n", info.name, info.value);
}
}
private:
span<const char> c_mySpan;
Base( span<const char> s ):c_mySpan(s) {}
Base(Base const&)=delete; // probably unsafe
};
Maintenant, votre dérivé ressemble à:
class DerivedA : public Base
{
public:
const SomeInfo c_myInfo[2] { {"NameA1", 1.1f}, {"NameA2", 1.2f} };
DerivedA() : Base(c_myInfo) {}
};
Cela a des frais généraux de deux pointeurs par Base
. Une table virtuelle utilise un pointeur, rend votre type abstrait, ajoute une indirection et ajoute une table globale par type Derived
.
Maintenant, en théorie, vous pouvez obtenir la surcharge de cela jusqu'à la longueur du tableau et supposer que les données du tableau commencent juste après Base
, mais c'est fragile, non portable et seulement utile si désespéré.
Bien que vous puissiez à juste titre vous méfier des modèles dans le code incorporé (comme vous devriez être de tout type de génération de code; la génération de code signifie que vous pouvez générer plus de O(1) binary from O(1) code). Le type de vocabulaire span est compact et ne doit être défini sur rien si vos paramètres de compilateur sont raisonnablement agressifs.
Que diriez-vous de CRTP + std :: array? Aucune variable supplémentaire, v-ptr ou appel de fonction virtuelle. std :: array est un wrapper très fin autour d'un tableau de style C. L'optimisation de classe de base vide garantit qu'aucun espace n'est gaspillé. Il me semble assez "élégant" :)
template<typename Derived>
class BaseT
{
public:
struct SomeInfo
{
const char *name;
const f32_t value;
};
void iterateInfo()
{
Derived* pDerived = static_cast<Derived*>(this);
for (const auto& i: pDerived->c_myInfo)
{
printf("Name: %s - Value: %f \n", i.name, i.value);
}
}
};
class DerivedA : public BaseT<DerivedA>
{
public:
const std::array<SomeInfo,2> c_myInfo { { {"NameA1", 1.1f}, {"NameA2", 1.2f} } };
};
class DerivedB : public BaseT<DerivedB>
{
public:
const std::array<SomeInfo, 3> c_myInfo { { {"NameB1", 2.1f}, {"NameB2", 2.2f}, {"NameB2", 2.3f} } };
};
Donc, si vous voulez vraiment garder vos données organisées comme elles sont, et je peux voir pourquoi vous le feriez dans la vraie vie:
Une façon avec C++ 17 serait de renvoyer un objet "view" représentant votre liste de contenu. Cela peut ensuite être utilisé dans une instruction C++ 11 for
. Vous pouvez écrire une fonction de base qui convertit start+len
dans une vue, vous n'avez donc pas besoin d'ajouter à la méthode virtuelle cruft.
Il n'est pas si difficile de créer un objet de vue compatible avec C++ 11 pour l'instruction. Vous pouvez également envisager d'utiliser les modèles for ++ pour C++ 98 qui peuvent prendre un itérateur de début et de fin: votre itérateur de démarrage est start
; l'itérateur final est start+len
.
Vous pouvez déplacer vos données dans un tableau à deux dimensions en dehors des classes et demander à chaque classe de renvoyer un index qui contient les données pertinentes.
struct SomeInfo
{
const char *name;
const f32_t value;
};
const vector<vector<SomeInfo>> masterStore{
{{"NameA1", 1.1f}, {"NameA2", 1.2f}},
{{"NameB1", 2.1f}, {"NameB2", 2.2f}, {"NameB2", 2.3f}}
};
class Base
{
public:
void iterateInfo()
{
// I would love to just write
// for(const auto& info : c_myInfo) {...}
u8_t len = 0;
auto index(getIndex());
for(const auto& data : masterStore[index])
{
DPRINTF("Name: %s - Value: %f \n", data.name, data.value);
}
}
virtual int getIndex() = 0;
};
class DerivedA : public Base
{
public:
int getIndex() override
{
return 0;
}
};
class DerivedB : public Base
{
public:
int getIndex() override
{
return 1;
}
};
DerivedA instanceA;
DerivedB instanceB;
instanceA.iterateInfo();
instanceB.iterateInfo();
Il suffit de faire en sorte que la fonction virtuelle renvoie directement une référence aux données (vous devez alors passer au vecteur - pas possible avec des types de tableau de style C ou de tableau de différentes tailles):
virtual const std::vector<SomeInfo>& getDerivedInfo() = 0;
ou si les pointeurs sont la seule option possible, comme plage de pointeurs (les itérateurs/adaptateurs de plage seraient préférés si possible - plus à ce sujet):
virtual std::pair<SomeInfo*, SomeInfo*> getDerivedInfo() = 0;
Pour faire fonctionner cette dernière méthode avec basé sur une plage pour la boucle: une façon consiste à créer un petit type de "vue de plage" qui a les fonctions begin()/end()
- une paire essentielle avec begin()/end()
template<class T>
struct ptr_range {
std::pair<T*, T*> range_;
auto begin(){return range_.first;}
auto end(){return range_.second;}
};
Puis construisez-le avec:
virtual ptr_range<SomeInfo> getDerivedInfo() override
{
return {std::begin(c_myInfo), std::end(c_myInfo)};
}
Il est facile de le rendre non modèle si un modèle n'est pas souhaité.