标签调度、可变参数模板、通用引用和遗漏的 const 说明符
Tag dispatching, variadic template, universal reference and the missed const specifier
请考虑以下示例(标签调度、可变参数模板、完美转发等,全部合而为一):
#include <iostream>
#include <utility>
#include <string>
struct A { };
struct B { };
void doIt(A&&, const std::string &) {
std::cout << "A-spec" << std::endl;
}
template<typename T, typename... Args>
void doIt(T&&, Args&&...) {
std::cout << "template" << std::endl;
}
template<typename T, typename... Args>
void fn(Args&&... args) {
doIt(T{}, std::forward<Args>(args)...);
}
int main() {
const std::string foo = "foo";
std::string bar = "bar";
fn<A>(foo);
fn<A>(bar);
fn<B>(foo);
}
在这种情况下,输出为:
A-spec
template
template
原因很明显,我不介意。
我想要实现的是在这两种情况下调用 doIt
函数的第一个实例,无论字符串是否具有 const 说明符。
当然,一个可能的解决方案是定义一个新的 doIt
和正确的原型,无论如何我想知道是否还有其他解决方案。
到目前为止,我已经尝试通过 add_const
获取它,但我很确定我错过了一些东西。
是否有任何可行的解决方案来静默添加 const 说明符并使其正常工作?
编辑
我已经更新了上面的示例,以便与实际问题更加一致。
此外,尽管答案很有趣,但我忘了引用这只是一个简化的例子,所以真正的问题不仅仅涉及std::string
。相反,可能发生(例如)对于标签 A
,参数是 int
和 const std::string &
,而对于标签 B
,参数是 float
, class C
的实例,依此类推。
因此,那些试图以某种方式使用 std::string
类型解决问题的答案将无法解决真正的问题,我很抱歉。
转发引用的重载很痛苦,因为它们非常非常贪婪。
如果 Args...
是可转换为 std::string
的单个参数,则一个选项是禁用模板版本:
template<typename... Args>
struct convertible_to_single_string {
static std::true_type help (std::string);
static std::false_type help (...);
using type = decltype(help(std::declval<Args>()...));
static constexpr auto value = type::value;
};
template<typename... Args>
std::enable_if_t<!convertible_to_single_string<Args...>::value, void>
doIt(int, Args&&...) {
std::cout << "template" << std::endl;
}
这也会调用 const char*
的字符串版本或将 user-defined 转换为 std::string
的任何内容。如果您不希望这样,还有其他可能性,例如从类型中删除所有引用和 cv-qualifiers 并检查它是否 std::is_same<std::string, T>
.
template<class String,
std::enable_if_t<std::is_same<std::decay_t<String>,std::string>{},int> =0
>
void doIt(int, String&&) {
std::cout << "std::string" << std::endl;
}
这使用转发引用,然后 SFINAE 仅在其中获取任何 string
类型。
如果你愿意将你的参数捆绑成一个元组,你也可以使用 usimg 标签调度。 is_same
可以替换为 is_convertible
,然后可以使用具有不同名称的辅助函数,实际上转换为 std::string
。
另一种方法是为标签保留第 2 层中的第一个 arg,然后手动计算出您想要的重载,并生成该标签类型。
引入两个独立的函数,这样它们就不会相互冲突,编译器也不会引发任何歧义错误:
void doIt(A&&, const std::string &)
{
std::cout << "A-spec" << std::endl;
}
template <typename T, typename... Args>
void doIt_template(T&&, Args&&...)
{
std::cout << "template" << std::endl;
}
优先考虑另外两个重载;首选的是尝试调用目标函数的专门 non-templated 版本:
template <typename T, typename... Args>
auto fn_impl(int, Args&&... args)
-> decltype(doIt(T{}, std::forward<Args>(args)...), void())
{
doIt(T{}, std::forward<Args>(args)...);
}
template <typename T, typename... Args>
void fn_impl(char, Args&&... args)
{
doIt_template(T{}, std::forward<Args>(args)...);
}
介绍一个单一的通用调度员:
template <typename T, typename... Args>
void fn(Args&&... args)
{
fn_impl<T>(0, std::forward<Args>(args)...);
}
请考虑以下示例(标签调度、可变参数模板、完美转发等,全部合而为一):
#include <iostream>
#include <utility>
#include <string>
struct A { };
struct B { };
void doIt(A&&, const std::string &) {
std::cout << "A-spec" << std::endl;
}
template<typename T, typename... Args>
void doIt(T&&, Args&&...) {
std::cout << "template" << std::endl;
}
template<typename T, typename... Args>
void fn(Args&&... args) {
doIt(T{}, std::forward<Args>(args)...);
}
int main() {
const std::string foo = "foo";
std::string bar = "bar";
fn<A>(foo);
fn<A>(bar);
fn<B>(foo);
}
在这种情况下,输出为:
A-spec
template
template
原因很明显,我不介意。
我想要实现的是在这两种情况下调用 doIt
函数的第一个实例,无论字符串是否具有 const 说明符。
当然,一个可能的解决方案是定义一个新的 doIt
和正确的原型,无论如何我想知道是否还有其他解决方案。
到目前为止,我已经尝试通过 add_const
获取它,但我很确定我错过了一些东西。
是否有任何可行的解决方案来静默添加 const 说明符并使其正常工作?
编辑
我已经更新了上面的示例,以便与实际问题更加一致。
此外,尽管答案很有趣,但我忘了引用这只是一个简化的例子,所以真正的问题不仅仅涉及std::string
。相反,可能发生(例如)对于标签 A
,参数是 int
和 const std::string &
,而对于标签 B
,参数是 float
, class C
的实例,依此类推。
因此,那些试图以某种方式使用 std::string
类型解决问题的答案将无法解决真正的问题,我很抱歉。
转发引用的重载很痛苦,因为它们非常非常贪婪。
如果 Args...
是可转换为 std::string
的单个参数,则一个选项是禁用模板版本:
template<typename... Args>
struct convertible_to_single_string {
static std::true_type help (std::string);
static std::false_type help (...);
using type = decltype(help(std::declval<Args>()...));
static constexpr auto value = type::value;
};
template<typename... Args>
std::enable_if_t<!convertible_to_single_string<Args...>::value, void>
doIt(int, Args&&...) {
std::cout << "template" << std::endl;
}
这也会调用 const char*
的字符串版本或将 user-defined 转换为 std::string
的任何内容。如果您不希望这样,还有其他可能性,例如从类型中删除所有引用和 cv-qualifiers 并检查它是否 std::is_same<std::string, T>
.
template<class String,
std::enable_if_t<std::is_same<std::decay_t<String>,std::string>{},int> =0
>
void doIt(int, String&&) {
std::cout << "std::string" << std::endl;
}
这使用转发引用,然后 SFINAE 仅在其中获取任何 string
类型。
如果你愿意将你的参数捆绑成一个元组,你也可以使用 usimg 标签调度。 is_same
可以替换为 is_convertible
,然后可以使用具有不同名称的辅助函数,实际上转换为 std::string
。
另一种方法是为标签保留第 2 层中的第一个 arg,然后手动计算出您想要的重载,并生成该标签类型。
引入两个独立的函数,这样它们就不会相互冲突,编译器也不会引发任何歧义错误:
void doIt(A&&, const std::string &)
{
std::cout << "A-spec" << std::endl;
}
template <typename T, typename... Args>
void doIt_template(T&&, Args&&...)
{
std::cout << "template" << std::endl;
}
优先考虑另外两个重载;首选的是尝试调用目标函数的专门 non-templated 版本:
template <typename T, typename... Args>
auto fn_impl(int, Args&&... args)
-> decltype(doIt(T{}, std::forward<Args>(args)...), void())
{
doIt(T{}, std::forward<Args>(args)...);
}
template <typename T, typename... Args>
void fn_impl(char, Args&&... args)
{
doIt_template(T{}, std::forward<Args>(args)...);
}
介绍一个单一的通用调度员:
template <typename T, typename... Args>
void fn(Args&&... args)
{
fn_impl<T>(0, std::forward<Args>(args)...);
}