The intent is to transform variadic template arguments from pair wise argument to list wise arguments; I mean like:
concept NameT = ...
concept ValueT = ...
template<NameT ...ArgName, ValueT ...ArgValue>
Item(ArgName&&... arg_name, ArgValue&&... arg_value) { ... }
std::vector<Item> items;
template<typename... Args>
void collect(Args&& ...args) {
items.emplace_back(arg1_name, arg2_name,..., arg1_value, arg2_value,...):
}
// usage like:
collect(arg1_name, arg1_value, arg2_name, arg2_value, ...);
But before I'll go into details, I'll show my testing/concept which is not working as expected, see godbolt
template<typename T> concept NameT = std::convertible_to<T, std::string_view>;
template<typename T> concept ValueT = std::convertible_to<T, int>;
struct Item {
template<NameT ...ArgName, ValueT ...ArgValue>
Item(ArgName&&... arg_name, ArgValue&&... arg_value)
{
((std::cout << std::forward<ArgName>(arg_name) << " : " << std::forward<ArgValue>(arg_value) << '\n'), ...);
}
};
int main() {
auto const v1 = Item("arg1", 42);
return 0;
}
results into:
candidate template ignored: constraints not satisfied [with ArgName = <>, ArgValue = <const char (&)[5], int>]
which I wouldn't really expect (empty ArgName constraint).
So I wrote a small helper to retain the API of name/value arg pairs - of collect()
and here I am, see godbolt
constexpr auto MAX_ARGS = 2;
template<typename T> concept ArgNameT = std::convertible_to<T, std::string_view>;
template<typename T> concept ArgValueT = std::convertible_to<T, int>;
struct item_value {
int value; // becomes variant
friend std::ostream& operator<<(std::ostream& os, item_value ival) {
os << ival.value;
return os;
}
};
struct item {
template<typename... ArgNames, typename... ArgValues>
item(std::string_view item_name_, ArgNames&& ...arg_names_, ArgValues&& ...arg_values_)
: item_name{ item_name_ }
, arg_names{ std::forward<ArgNames>(arg_names_)...}
, arg_values{ std::forward<ArgValues>(arg_values_)...}
{}
std::string item_name;
// This memory layout to minimize footprint
std::array<std::string_view, MAX_ARGS> arg_names;
std::array<item_value, MAX_ARGS> arg_values;
};
struct logger {
static logger& instance() {
static logger static_instance;
return static_instance;
};
template<typename... Args> void add_item(std::string_view name, Args&& ...args);
template<typename... Args> void do_add_item(std::string_view name, int payload, Args&& ...args);
using arg_type = struct { std::string_view name; item_value value; };
auto make_arg_types() { return std::tuple<>(); }
template<ArgNameT FirstName, ArgValueT FirstValue, typename... Args>
auto make_arg_types(const FirstName &name, const FirstValue &value, const Args&... args) {
return std::tuple_cat(
std::make_tuple(arg_type{name, value}),
make_arg_types(std::forward<Args>(args)...)
);
}
std::vector<item> items;
};
// implementation
template<typename... Args>
void logger::add_item(std::string_view item_name, Args&& ...args) {
int payload = 69;
do_add_item(item_name, payload, std::forward<Args>(args)...);
}
template<typename... Args>
void logger::do_add_item(std::string_view item_name, int payload, Args&& ...args) {
auto arg_types = make_arg_types(std::forward<Args>(args)...);
// so far, we have arg name/value tuples ...
std::apply([](auto&&... arg_type) {
((std::cout << arg_type.name << " : " << arg_type.value << '\n'), ...);
}, arg_types);
// ... but item's CTor API is different
//items.emplace_back(item_name, payload, ..., ...); // How to?
}
template<typename... Args>
void collect(std::string_view item_name, Args&& ...args) { // free function
logger::instance().add_item(item_name, std::forward<Args>(args)...);
}
int main() {
collect("Superman"); // no arg
collect("Batman", "is bad"sv, 666); // single arg
// ... 2 name/value args etc.
return 0;
}
The problem with the collect()
's API ist solved, but item
class constructor argument is different (... and rises as new problem), hence I can't emplace them into logger's vector items
.
How to emplace them?
template<NameT ...ArgName, ValueT ...ArgValue>
Item(ArgName&&... arg_name, ArgValue&&... arg_value)
{
}
this is, in practice, not allowed -- you can't have 2 parameter pack function arguments.
The compiler has no way to know which argument is in which pack, and explicitly refuses to guess.
One approach is to take two tuples:
template<NameT ...ArgName, ValueT ...ArgValue>
Item(std::tuple<ArgName...> arg_name, std::tuple<ArgValue...> arg_value)
{
}
another is to take a list of pair-like things.
You can also take one huge list, and split it up manually. You have code that splits it by (a,b,a,b,a,b), but you can do (a,a,a,b,b,b) by halving the list, sticking each half in a std::forward_as_tuple
, then pairing them up there.
This splits arguments into front and back:
template<std::size_t...Is>
auto splitArgsImpl( std::index_sequence<Is...>, auto&&... args ) {
auto tmpTuple = std::forward_as_tuple( decltype(args)(args)... );
return std::make_tuple(
std::forward_as_tuple( std::get<Is>(tmpTuple)... ),
std::forward_as_tuple( std::get<Is + sizeof...(Is)>(tmpTuple)... )
);
}
auto splitArgs( auto&&... args ) {
return splitArgsImpl( std::make_index_sequence<sizeof...(args)/2>{}, decltype(args)(args)... );
}
then something like:
Item makeItem(auto&&...args) {
auto split = splitArgs(decltype(args)(args)...);
return std::make_from_tuple<Item>( std::move(split) );
}
template<class...Args> requires (sizeof...(Args) >= 2)
Item( Args&&...args ):Item( makeItem(std::forward<Args>(args)...) ) {}
Item( Item&& ) = default;
Item( Item const& ) = default;
template<class Names, class Values>
Item( std::tuple<Names&&...>&& names, std::tuple<Values&&...>&& values ) {
// code
}