Første gang jeg forsøgte at lave template specializations af en metode, fik jeg fejlen explicit specialization in non-namespace scope....
Jeg troede derfor at det ikke var muligt (i hvert fald indenfor C++17-standarden). Heldigvis er begrænsningen ikke så stor som den kan se ud.
Lad os tage udgangspunkt i en struct fra en tidligere artikel:
enum class character_class { rogue, warrior, sorcerer, };
enum class property
{
damage,
health,
character_class,
level,
is_npc,
};
struct character
{
private:
double damage;
double health;
character_class type;
unsigned int level;
bool is_npc;
public:
// ... getters/setters tilføjes her ...
};
Der er altså tale om en data-struktur med flere forskellige typer data. For at tilgå data kan vi definere et interface således:
template <property p, typename T> constexpr void set(T value); template <property p, typename T> constexpr T get() const;
Her vil det være oplagt at bruge template-specialiseringer, så lad os prøve at lave en specialisering til set-metode for property::damage:
struct character
{
private:
double damage;
double health;
character_class type;
unsigned int level;
bool is_npc;
public:
template <property p, typename T> constexpr void set(T value);
template <property p, typename T> constexpr T get() const;
// Fejl!
template <> constexpr void set<property::damage,double>(double value)
{
damage = value;
}
};
Men her får vi den omtalte fejl: explicit specialization in non-namespace scope 'struct character'. Betyder det så, at vi skal opgive denne fremgangsmåde? Nej!
I stedet skal koden bare omskrives, så selve specialiseringen ligger udenfor klassen - som fejlen jo egentlig også indikerer:
struct character
{
private:
double damage;
double health;
character_class type;
unsigned int level;
bool is_npc;
public:
template <property p, typename T> constexpr void set(T value);
template <property p, typename T> constexpr T get() const;
};
template <> constexpr void character::set<property::damage,double>(double value)
{
damage = value;
}
Nu er selve metode-prototypen med template parametre defineret indenfor klassen, mens specialiseringen står udenfor class scope. Ligeledes kan de andre specialiseringer laves:
template <> constexpr void character::set<property::damage,double>(double value) { damage = value; }
template <> constexpr void character::set<property::health,double>(double value) { health = value; }
template <> constexpr void character::set<property::character_class,character_class>(character_class value) { type = value; }
template <> constexpr void character::set<property::level,unsigned int>(unsigned int value) { level = value; }
template <> constexpr void character::set<property::level,int>(int value) { level = value; } // Overload for signed int
template <> constexpr void character::set<property::is_npc,bool>(bool value) { is_npc = value; }
template <> constexpr double character::get<property::damage,double>() const { return damage; }
template <> constexpr double character::get<property::health,double>() const { return health; }
template <> constexpr character_class character::get<property::character_class,character_class>() const { return type; }
template <> constexpr unsigned int character::get<property::level,unsigned int>() const { return level; }
template <> constexpr bool character::get<property::is_npc,bool>() const { return is_npc; }
Dette design vil ikke altid være optimalt - f.eks. hvis der er mange felter af samme type, er der meget smartere alternativer som vi ser på omlidt. Til gengæld viser det, hvordan template-specialiseringer kan laves til metoder, hvilket kan være nyttigt.
Det er altså ikke bare muligt, men også ret nemt at lave template-specialiseringer af metoder - de skal bare flyttes ud af class scope.
Til slut vil jeg vise et eksempel på brug af template-specialiseringer, som skalerer bedre hvis du har mange felter af samme type:
enum class property
{
damage,
health,
mana,
level,
gold,
experience,
};
struct character
{
private:
std::map<property,double> _doubles;
std::map<property,int> _integers;
template <typename T> inline std::map<property,T>& get_map();
template <typename T> inline const std::map<property,T>& get_map() const;
public:
template <property p, typename T> constexpr void set(T value)
{
get_map<T>()[p] = value;
}
template <property p, typename T> constexpr T get() const
{
const auto& map = get_map<T>();
auto i = map.find(p);
return (i != map.end()) ? i->second : std::numeric_limits<T>::quiet_NaN();
}
};
template <> inline auto character::get_map<double>() -> decltype(_doubles)& { return _doubles; }
template <> inline auto character::get_map<double>() const -> const decltype(_doubles)& { return _doubles; }
template <> inline auto character::get_map<int>() -> decltype(_integers)& { return _integers; }
template <> inline auto character::get_map<int>() const -> const decltype(_integers)& { return _integers; }
Med dette design kan nye property-elementer tilføjes ved kun at udvide denne enum!
Og det bruges ligeså nemt som tidligere:
character player; player.set<property::damage>(10.0); player.set<property::health>(100.0); player.set<property::mana>(50.0); player.set<property::level>(4); player.set<property::gold>(500); player.set<property::experience>(120000);
Til gengæld er der ikke noget type-check knyttet til property.
Dette kan klares ved at bruge en assert_type-funktion som beskrevet her.