C++可变参数模板函数参数包推导失败及简化调用需求
问题描述
给定以下代码:
template <class P> concept Pair = requires(P pair) { typename P::first_type; typename P::second_type; pair.first; pair.second; { pair.first } -> std::same_as<typename P::first_type &>; { pair.second } -> std::same_as<typename P::second_type &>; }; template <typename T> setTag(const std::string_view key, const T &value) { foo(key.data(), value); // things in here } template <Pair... Pairs> void setTags(Pairs... pairs) { ((setTag(std::get<0>(pairs), std::get<1>(pairs)), ...)); }
当前调用setTags时,必须为每个参数显式构造pair:
setTags(std::make_pair("key1", 1), std::make_pair("key2", "example"), std::make_pair("key3", std::set{1, 2}));
我希望能无需显式调用make_pair,直接通过如下方式调用:
setTags({"key1", 1}, {"key2", "example"}, {"key3", std::set{1, 2}});
但这会触发编译错误:
note: candidate template ignored: substitution failure: deduced incomplete pack <(no value), (no value), (no value)> for template parameter 'Pairs'
原因是编译器无法推导参数包中参数的类型,我引入了C++20 Concept来约束类型,但并未解决问题。
尝试过的方案
- 使用
nlohmann_json等JSON库接收参数,但会引入不必要的依赖。 - 使用
std::initializer_list,示例如下:
但该方案会丢失泛型性,要求所有值的类型必须相同:template <typename T> void setTags(std::initializer_list<std::pair<std::string,T>> pairs) { for(const auto &[key, value] : pairs){ setTag(key, value); } }setTags<int>({{"key1", 1}, {"key2",2}});
编辑补充
根据建议,我编写了一个验证std::pair、std::get且first为字符串类型的Concept:
template <typename P> concept KeyValuePair = requires(P pair) { typename std::decay_t<P>::first_type; typename std::decay_t<P>::second_type; { std::get<0>(pair) } -> std::convertible_to<typename std::decay_t<P>::first_type>; { std::get<1>(pair) } -> std::convertible_to<typename std::decay_t<P>::second_type>; std::is_convertible_v<typename std::decay_t<P>::first_type, std::string>; };
以及使用CTAD的简化调用方式:
setTags(std::pair("key1", 1), std::pair("key2", "example"), std::pair("key3", std::set{1, 2}));
但仍希望实现无需显式编写std::pair的调用方式。
解决方案
要支持直接用初始化列表{key, value}作为参数传递给setTags,核心是让编译器能把每个初始化列表推导为合适的std::pair类型,以下是两种简洁可行的实现方式:
方法1:利用std::pair的CTAD特性重载参数包
直接将模板参数包约束为std::pair的实例,编译器会自动通过CTAD推导每个初始化列表对应的std::pair类型:
template <typename... K, typename... V> void setTags(std::pair<K, V>... pairs) { ((setTag(std::get<0>(pairs), std::get<1>(pairs)), ...)); }
这种方式最直接,不需要修改原有Concept或复杂逻辑,完全适配你的期望调用方式。
方法2:优化Concept适配初始化列表推导
如果想保留Concept约束,需要调整Concept使其兼容临时pair对象和初始化列表推导,同时让参数包支持转发引用:
template <typename P> concept KeyValuePair = requires(P pair) { // 放宽约束,只要能通过std::get获取可转换为字符串的键即可 { std::get<0>(pair) } -> std::convertible_to<std::string_view>; }; template <KeyValuePair... Pairs> void setTags(Pairs&&... pairs) { ((setTag( std::get<0>(std::forward<Pairs>(pairs)), std::get<1>(std::forward<Pairs>(pairs)) ), ...)); }
此时编译器会自动将每个{key, value}推导为临时std::pair对象,并通过Concept校验。
验证调用
使用上述任意一种方法后,都可以直接按期望方式调用:
setTags({"key1", 1}, {"key2", "example"}, {"key3", std::set{1, 2}});
编译器会自动推导每个初始化列表对应的std::pair类型,既保留泛型性,又不需要显式构造pair对象。
内容的提问来源于stack exchange,提问作者asm4ever
相关产品推荐
相关产品推荐

