当然,这是可能的。但是,在我解释解决方案之前,请允许我建议您通过坚持使用特定界面(在我看来)实际上并不比替代方案更干净,从而使问题变得不必要地困难。从本质上讲,您要求fun<arg>
按arg
价值或按参考取值,具体取决于哪一个实际上格式正确。在 的情况下a1
,只能按值取值;它不能被引用,因为它没有静态存储持续时间。在 的情况下a2
,由于未声明,因此无法按值获取constexpr
,但可以通过引用获取,因为它具有静态存储持续时间。
使用您提议的版本的代码fun
很难阅读,因为读者看到fun<arg>
后,不会立即知道arg
是按值还是按引用。读者必须根据读者自己的知识来推断它是哪一个,是通过值还是通过引用arg
来允许的非类型模板参数。此外,一些参数可能符合任一条件,在这种情况下,读者还必须知道实现者为这种情况选择了哪个默认值,才能知道发生了什么。fun
同样,这只是我的观点:如果你编写单独的函数,可能会更简单,也许调用它们fun_val
and fun_ref
, where fun_val<a1>()
andfun_ref<a2>()
是格式良好的。为此,我们应该定义两个包装类,一个通过值获取参数,另一个通过引用:
template <typename T>
struct wrap_value {
using value_type = T;
T thing;
constexpr wrap_value(T thing) : thing(thing) {}
};
template <typename T>
wrap_value(const T&) -> wrap_value<T>;
template <typename T>
struct wrap_reference {
using value_type = T;
const T& thing;
constexpr wrap_reference(const T& thing) : thing(thing) {}
};
template <typename T>
wrap_reference(const T&) -> wrap_reference<T>;
template <wrap_value V>
void fun_val() {
std::cout << "value\n";
}
template <wrap_reference V>
void fun_ref() {
std::cout << "reference\n";
}
struct X {
int a;
};
int main() {
constexpr auto a1 = &X::a;
static const auto a2 = &X::a;
static const int x = 42;
fun_val<a1>(); // OK
fun_ref<a1>(); // Error
fun_val<a2>(); // Error
fun_ref<a2>(); // OK
fun_val<x>(); // OK; uses value of x
fun_ref<x>(); // OK; uses address of x
}
现在,如果您坚持使用单个 name fun
,那么关键是要识别它a1
并a2
具有相同的类型,因此 CTAD 的单个应用程序将永远无法找出正确的包装器类型以使调用格式正确。相反,您必须将 SFINAE 与两个重载一起使用:对于给定模板参数无效的一个(因为它按值(resp.reference)获取参数,而不能按值(resp.reference)获取)被丢弃。基本上,将上述示例中的fun_val
和都重命名为:fun_ref
fun
template <wrap_value V>
void fun() {
std::cout << "value\n";
}
template <wrap_reference V>
void fun() {
std::cout << "reference\n";
}
a1
这在and的情况下工作得很好a2
,只有两个重载之一是候选者。但在 的情况下x
,它将是模棱两可的。假设您想在这种情况下强制选择按值重载。我们可以通过插入一个使按引用重载不是候选对象的约束来做到这一点:
template <wrap_reference V> requires(!requires { fun<wrap_value<typename decltype(V)::value_type>(V.thing)>(); })
void fun() {
std::cout << "reference\n";
}
您可以在此处查看完整的工作示例。