1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C++20で名前付きタプルを作る

Last updated at Posted at 2024-06-08

はじめに

この記事は身内で行ったTemplate勉強会用にサンプルとして作った名前付きタプルの解説を行ったものです。
元々投稿するつもりはなかったのですが、備忘録的な感じで残しておこうと思います。

この記事のゴール

これをやります。

int main()
{
	auto tuple = MakeNamedTuple<"a","b","c">(1, 1.2, "test");

	std::cout << get<"a">(tuple) << "\n";		//1
	std::cout << get<"b">(tuple) << "\n";		//1.2
	std::cout << get<"c">(tuple) << "\n";		//test
 }

テンプレートパラメータに文字を突っ込む

通常、テンプレートパラメータに直接文字列を入れることはできません。
しかし、C++20からはユーザー定義型を非型テンプレートパラメータに入れることが出来る(制限あり)ため、コンストラクタで文字列から型を推論させることで疑似的にテンプレートパラメータに突っ込むことができます。
今回はこれを名前の型として使用します。

/// @brief 文字の型かどうかを判定するコンセプト
///	@tparam Type 判定する型
template <typename Type>
concept CharacterType =
std::is_same_v<char, Type> ||
std::is_same_v<wchar_t, Type> ||
std::is_same_v<char8_t, Type> ||
std::is_same_v<char16_t, Type> ||
std::is_same_v<char32_t, Type>;

/// @brief コンパイル時文字列を扱うクラステンプレート
/// @tparam CharType 文字の型
/// @tparam Size サイズ
template <CharacterType CharType, size_t Size>
class StringParameter :
	public std::array<CharType, Size>
{
	static_assert(Size > 0, "サイズは1以上である必要があります");
public:
	/// @brief コンストラクタ
	/// @param data サイズ付き文字列のポインタ
	consteval StringParameter(const CharType(&data)[Size])
	{
		std::copy(data, data + Size, this->begin());
		this->back() = CharType();
	}
};

名前付きタプルのクラステンプレートを作る

次に実際に値を入れるコンテナのクラステンプレートを用意します。
名前と型はペアであってほしいので、ホルダーのクラステンプレートであるNamedTypeを作成し、それの可変長のみを許容するように特殊化してNamedTupleを作成します。
また、明示的に名前をメモリ上で管理する必要はないため、std::tupleを継承して値のみ保持してもらいます。

/// @brief 名前と型を持つクラステンプレート
/// @tparam ElemName 要素の名前
/// @tparam ElemType 要素の型
template <StringParameter ElemName, typename ElemType>
class NamedType
{
public:
	/// @brief 名前
	static constexpr auto name = ElemName;

	/// @brief 型
	using Type = ElemType;
};

/// @brief 名前付きタプル(プライマリ)
/// @tparam Containers NamedTypeのリスト
template <typename... Containers>
class NamedTuple;

/// @brief 名前付きタプル
///	details NamedTypeのリストとして展開した場合
/// @tparam Names 名前のリスト
/// @tparam Types 型のリスト
template<StringParameter... Names, typename... Types>
class NamedTuple<NamedType<Names, Types>...> :
	public std::tuple<Types...>
{
public:
	///@brief 継承コンストラクタ
	using std::tuple<Types...>::tuple;
};

名前からインデックスに変換する

このままでは名前を使って取得ができません。
そこで名前を実際にタプル上で紐づけられているインデックスに変換する仕組みを作成します。
どのインデックスに紐づけられているかはテンプレートの再帰と特殊化によるパターンマッチングで比較的簡単に導出することが出来ます。

/// @brief 名前から名前付きタプルの要素のインデックスと型を取得するクラステンプレート(プライマリ)
/// @tparam Index インデックス(最初から探索する場合は0を入れる)
/// @tparam Name 名前
/// @tparam Containers 型リスト
template <size_t Index, StringParameter Name, typename... Containers>
class NamedTupleLookup;

/// @brief 名前から名前付きタプルの要素のインデックスと型を取得するクラステンプレート
///	@details NamedTypeのリストとして展開した場合
///	@tparam Index インデックス(最初から探索する場合は0を入れる)
///	@tparam Name 名前
///	@tparam ElemName 最初の要素の名前
///	@tparam ElemType 最初の要素の型
///	@tparam Containers 残りの要素のリスト
template <size_t Index, StringParameter Name, StringParameter ElemName, typename ElemType, typename... Containers>
class NamedTupleLookup<Index, Name, NamedType<ElemName, ElemType>, Containers...>
{
	/// @brief 再帰的に探索
	using Next = NamedTupleLookup<Index + 1, Name, Containers...>;
public:
	/// @brief 要素の型
	using Type = typename Next::Type;

	/// @brief 要素のインデックス
	static constexpr size_t index = Next::index;
};

/// @brief 名前から名前付きタプルの要素のインデックスと型を取得するクラステンプレート
///	@details 名前が一致した場合
/// @tparam Index インデックス
/// @tparam Name 名前
/// @tparam ElemType 要素の型
/// @tparam Containers 残りの要素型のリスト
template <size_t Index, StringParameter Name, typename ElemType, typename... Containers>
class NamedTupleLookup<Index, Name, NamedType<Name, ElemType>, Containers...>
{
public:
	/// @brief 要素の型
	using Type = ElemType;

	/// @brief 要素のインデックス
	static constexpr size_t index = Index;
};

/// @brief NamedTupleの要素のインデックスを取得する変数テンプレート
/// @tparam Name 名前
/// @tparam Containers 要素型のリスト
template <StringParameter Name, typename... Containers>
constexpr size_t namedTupleIndex = NamedTupleLookup<0, Name, Containers...>::index;

ヘルパ関数を作る

あとは実際に取得したりNamedTupleを簡単に作成したりするためのヘルパ関数群を作成すれば完成です。

/// @brief 指定した名前の要素を取得する関数テンプレート
/// @tparam Name 名前
/// @tparam Containers 要素型のリスト
/// @param tuple 名前付きタプル
/// @return 要素の参照
template <StringParameter Name, typename... Containers>
decltype(auto) get(NamedTuple<Containers...>& tuple)
{
	return std::get<namedTupleIndex<Name, Containers...>>(tuple);
}

/// @brief 指定した名前の要素を取得する関数テンプレート
///	@details const版
/// @tparam Name 名前
/// @tparam Containers 要素型のリスト
/// @param tuple 名前付きタプル
/// @return 要素の参照
template <StringParameter Name, typename... Containers>
[[nodiscard]]
decltype(auto) get(const NamedTuple<Containers...>& tuple)
{
	return std::get<namedTupleIndex<Name, Containers...>>(tuple);
}

/// @brief 名前付きタプルを作成するヘルパー関数テンプレート
///	@details 名前と型が一対一に対応していることを要求する
/// @tparam Names 名前のリスト
/// @tparam Types 型のリスト
/// @param args 要素を構成する値
/// @return 名前付きタプル
template <StringParameter... Names, typename... Types>
	requires (sizeof...(Names) == sizeof...(Types))
[[nodiscard]]
decltype(auto) MakeNamedTuple(Types&&... args)
{
	return NamedTuple<NamedType<Names, Types>...>(std::forward<Types>(args)...);
}

コード全体像

今まで書いた名前付きタプルに少し制約などを足してまとめたコード以下になります。

#include <stddef.h>

#include <algorithm>
#include <array>
#include <iostream>
#include <tuple>
#include <type_traits>
#include <utility>

/// @brief 文字の型かどうかを判定するコンセプト
///	@tparam Type 判定する型
template <typename Type>
concept CharacterType =
std::is_same_v<char, Type> ||
std::is_same_v<wchar_t, Type> ||
std::is_same_v<char8_t, Type> ||
std::is_same_v<char16_t, Type> ||
std::is_same_v<char32_t, Type>;

/// @brief コンパイル時文字列を扱うクラステンプレート
/// @tparam CharType 文字の型
/// @tparam Size サイズ
template <CharacterType CharType, size_t Size>
class StringParameter :
	public std::array<CharType, Size>
{
	static_assert(Size > 0, "サイズは1以上である必要があります");
public:
	/// @brief コンストラクタ
	/// @param data サイズ付き文字列のポインタ
	consteval StringParameter(const CharType(&data)[Size])
	{
		std::copy(data, data + Size, this->begin());
		this->back() = CharType();
	}
};

/// @brief String1とString2が同じ文字列かどうかを判定する
/// @tparam String1 文字列1
/// @tparam String2 文字列2
template <StringParameter String1, StringParameter String2>
constexpr bool isSameStringV = false;

/// @brief isSameStringVに同じ文字列を渡した場合の特殊化
/// @tparam String 文字列
template <StringParameter String>
constexpr bool isSameStringV<String, String> = true;

/// @brief 文字列リストの中に重複がないかどうかを判定する
/// @tparam String 比較対象の文字列
/// @tparam Strings 他の文字列
template <StringParameter String, StringParameter... Strings>
constexpr bool isStringSetV = (!isSameStringV<String, Strings> && ...) && isStringSetV<Strings...>;

/// @brief isStringSetVに一つしか文字列を渡さなかった場合の特殊化
/// @tparam String 文字列
template <StringParameter String>
constexpr bool isStringSetV<String> = true;

/// @brief 名前と型を持つクラステンプレート
/// @tparam ElemName 要素の名前
/// @tparam ElemType 要素の型
template <StringParameter ElemName, typename ElemType>
class NamedType
{
public:
	/// @brief 名前
	static constexpr auto name = ElemName;

	/// @brief 型
	using Type = ElemType;
};

/// @brief 名前付きタプル(プライマリ)
/// @tparam Containers NamedTypeのリスト
template <typename... Containers>
class NamedTuple;

/// @brief 名前付きタプル
///	details NamedTypeのリストとして展開した場合
/// @tparam Names 名前のリスト
/// @tparam Types 型のリスト
template<StringParameter... Names, typename... Types>
class NamedTuple<NamedType<Names, Types>...> :
	public std::tuple<Types...>
{
	static_assert(isStringSetV<Names...>, "名前が重複しています");
public:
	///@brief 継承コンストラクタ
	using std::tuple<Types...>::tuple;
};

/// @brief 名前から名前付きタプルの要素のインデックスと型を取得するクラステンプレート(プライマリ)
/// @tparam Index インデックス(最初から探索する場合は0を入れる)
/// @tparam Name 名前
/// @tparam Containers 型リスト
template <size_t Index, StringParameter Name, typename... Containers>
class NamedTupleLookup;

/// @brief 名前から名前付きタプルの要素のインデックスと型を取得するクラステンプレート
///	@details NamedTypeのリストとして展開した場合
///	@tparam Index インデックス(最初から探索する場合は0を入れる)
///	@tparam Name 名前
///	@tparam ElemName 最初の要素の名前
///	@tparam ElemType 最初の要素の型
///	@tparam Containers 残りの要素のリスト
template <size_t Index, StringParameter Name, StringParameter ElemName, typename ElemType, typename... Containers>
class NamedTupleLookup<Index, Name, NamedType<ElemName, ElemType>, Containers...>
{
	/// @brief 再帰的に探索
	using Next = NamedTupleLookup<Index + 1, Name, Containers...>;
public:
	/// @brief 要素の型
	using Type = typename Next::Type;

	/// @brief 要素のインデックス
	static constexpr size_t index = Next::index;
};

/// @brief 名前から名前付きタプルの要素のインデックスと型を取得するクラステンプレート
///	@details 名前が一致した場合
/// @tparam Index インデックス
/// @tparam Name 名前
/// @tparam ElemType 要素の型
/// @tparam Containers 残りの要素型のリスト
template <size_t Index, StringParameter Name, typename ElemType, typename... Containers>
class NamedTupleLookup<Index, Name, NamedType<Name, ElemType>, Containers...>
{
public:
	/// @brief 要素の型
	using Type = ElemType;

	/// @brief 要素のインデックス
	static constexpr size_t index = Index;
};

/// @brief NamedTupleの要素のインデックスを取得する変数テンプレート
/// @tparam Name 名前
/// @tparam Containers 要素型のリスト
template <StringParameter Name, typename... Containers>
constexpr size_t namedTupleIndex = NamedTupleLookup<0, Name, Containers...>::index;

/// @brief 指定した名前の要素を取得する関数テンプレート
/// @tparam Name 名前
/// @tparam Containers 要素型のリスト
/// @param tuple 名前付きタプル
/// @return 要素の参照
template <StringParameter Name, typename... Containers>
decltype(auto) get(NamedTuple<Containers...>& tuple)
{
	return std::get<namedTupleIndex<Name, Containers...>>(tuple);
}

/// @brief 指定した名前の要素を取得する関数テンプレート
///	@details const版
/// @tparam Name 名前
/// @tparam Containers 要素型のリスト
/// @param tuple 名前付きタプル
/// @return 要素の参照
template <StringParameter Name, typename... Containers>
[[nodiscard]]
decltype(auto) get(const NamedTuple<Containers...>& tuple)
{
	return std::get<namedTupleIndex<Name, Containers...>>(tuple);
}

/// @brief 名前付きタプルを作成するヘルパー関数テンプレート
///	@details 名前と型が一対一に対応していることを要求する
/// @tparam Names 名前のリスト
/// @tparam Types 型のリスト
/// @param args 要素を構成する値
/// @return 名前付きタプル
template <StringParameter... Names, typename... Types>
	requires (sizeof...(Names) == sizeof...(Types))
[[nodiscard]]
decltype(auto) MakeNamedTuple(Types&&... args)
{
	return NamedTuple<NamedType<Names, Types>...>(std::forward<Types>(args)...);
}

int main()
{
	auto tuple = MakeNamedTuple<"a","b","c">(1, 1.2, "test");
	
	std::cout << get<"a">(tuple) << "\n";		//1
	std::cout << get<"b">(tuple) << "\n";		//1.2
	std::cout << get<"c">(tuple) << "\n";		//test
 }

まとめ

以上が名前付きタプルのざっくりとした作り方になります。
結構コードを雑に組んでしまったので、もしかしたらとんでもないミスなどあるかもしれませんが、何卒ご容赦ください……!

参考記事

以下の記事を参考にさせていただきました。
ありがとうございます!

追記

「はじめに」で解説って書いてるけど、改めて読んでみるとほぼ解説してないですね……

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?