首页 > 解决方案 > 一个“constexpr”标记的变量和一个静态存储持续时间的变量是否可以通过带有推导指南的类类型进行存储?

问题描述

考虑以下代码:

template <typename T>
struct wrap {
    T thing;
    constexpr wrap(T thing) : thing(thing) {}
};
template <typename T>
wrap(const T&) -> wrap<T>;

template <wrap V>
void fun();

struct X {
    int a;
};

int main() {
    constexpr auto a1 = &X::a;
    static const auto a2 = &X::a;
    fun<a1>();
    fun<a2>(); // Doesn't compile
}

现在的问题是,无需显式声明模板参数即可成功通过a1,但没有静态存储持续时间wrapa2

如果我将上述扣除指南更改为:

template <typename T>
wrap(const T&) -> wrap<const T&>; // <-- Note the 'const T&' here instead of plain 'T'

然后一个可以通过a2但不是a1

如果可能的话,如何修改上面的代码,以便能够同时传递a1a2分别传递,而不必显式声明像fun<wrap<decltype(a1)>{a1}>()or之类的类型fun<wrap<const decltype(a2)&>{a2}>()

标签: c++templatesc++20deduction-guide

解决方案


当然,这是可能的。但是,在我解释解决方案之前,请允许我建议您通过坚持使用特定界面(在我看来)实际上并不比替代方案更干净,从而使问题变得不必要地困难。从本质上讲,您要求fun<arg>arg价值或按参考取值,具体取决于哪一个实际上格式正确。在 的情况下a1,只能按值取值;它不能被引用,因为它没有静态存储持续时间。在 的情况下a2,由于未声明,因此无法按值获取constexpr,但可以通过引用获取,因为它具有静态存储持续时间。

使用您提议的版本的代码fun很难阅读,因为读者看到fun<arg>后,不会立即知道arg是按值还是按引用。读者必须根据读者自己的知识来推断它是哪一个,是通过还是通过引用arg来允许的非类型模板参数。此外,一些参数可能符合任一条件,在这种情况下,读者还必须知道实现者为这种情况选择了哪个默认值,才能知道发生了什么。fun

同样,这只是我的观点:如果你编写单独的函数,可能会更简单,也许调用它们fun_valand 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,那么关键是要识别它a1a2具有相同的类型,因此 CTAD 的单个应用程序将永远无法找出正确的包装器类型以使调用格式正确。相反,您必须将 SFINAE 与两个重载一起使用:对于给定模板参数无效的一个(因为它按值(resp.reference)获取参数,而不能按值(resp.reference)获取)被丢弃。基本上,将上述示例中的fun_val和都重命名为:fun_reffun

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";
}

您可以在此处查看完整的工作示例。


推荐阅读