Nighthawk.dk

C++: Template-specialiseringer i class scope

Skrevet d. 24. March 2023 af Claus Nielsen.

Det er ikke tilladt at lave explicit specializations i class scope, men det betyder ikke, at der ikke kan laves specialiseringer af metoder!

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.

Relaterede ressourcer
C++ Templates: The Complete Guide

D. Vandevoorde, N. Josuttis, D. Gregor,
Addison-Wesley Professional, 2nd ed.

C++ Primer

S. Lippman, J. Lajoie, B. Moo,
Addison-Wesley Professional, 5th ed.

×