“constexpr”标记的变量和静态存储持续时间的变量是否都允许通过带有推导的类类型进行存储?

Can both a 'constexpr'-marked variable and a variable of static-storage duration allowed to be stored through a class type with a deduction guide?

提问人:lekner 提问时间:11/11/2021 更新时间:11/11/2021 访问量:75

问:

请考虑以下代码段:

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,但 a2 则不行,因为 a2 具有静态存储持续时间

如果我将上面的扣除指南更改为此:

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

然后可以通过 a2 但不能通过 a1

如果可能的话,如何修改上面的代码,以便能够分别传递两者,而不必显式声明像或这样的类型?a1a2fun<wrap<decltype(a1)>{a1}>()fun<wrap<const decltype(a2)&>{a2}>()

C++ 模板 C++20 演绎指南

评论

2赞 Nicol Bolas 11/11/2021
有没有理由想工作?即使在特定情况下,您可以在常量表达式中使用非变量,也没有理由这样做。所以只要让人们像正常人一样使用。a2constexprconstexpr

答:

2赞 Brian Bi 11/11/2021 #1

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

使用您建议的版本的代码很难阅读,因为读者看到 ,不会立即知道是按值还是按引用获取。读者必须根据读者自己对是否是允许的非类型模板参数的了解,通过引用来推断它是哪一个。此外,某些论点可能符合其中任何一个条件,在这种情况下,读者还必须知道实现者为该情况选择了哪个默认值,以便知道发生了什么。funfun<arg>argargfun

同样,这只是我的观点:如果您编写单独的函数,也许将它们称为 和 , where 和 格式良好,那会简单得多。为此,我们应该定义两个包装类,一个通过值获取参数,另一个通过引用:fun_valfun_reffun_val<a1>()fun_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
}

现在,如果您坚持使用单个名称,那么关键是要识别它并具有相同的类型,因此 CTAD 的单个应用程序将永远无法找出正确的包装器类型来使调用格式正确。相反,您必须将 SFINAE 与两个重载一起使用:一个对给定模板参数无效的重载(因为它按值(或引用)获取参数,而不能按值(或引用)获取参数)将被丢弃。基本上,在上面的示例中将两者重命名为:funa1a2fun_valfun_reffun

template <wrap_value V>
void fun() {
    std::cout << "value\n";
}

template <wrap_reference V>
void fun() {
    std::cout << "reference\n";
}

这在 和 的情况下工作正常,其中只有两个重载中的一个是候选的。但在 的情况下,这将是模棱两可的。假设在这种情况下,您要强制选择按值重载。我们可以通过插入一个约束来做到这一点,该约束使 by-reference 重载不是候选重载:a1a2x

template <wrap_reference V> requires(!requires { fun<wrap_value<typename decltype(V)::value_type>(V.thing)>(); })
void fun() {
    std::cout << "reference\n";
}

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