如何在 T = void 时禁用返回 T& 的成员函数
结论
不需要额外引入模板参数 U。成员函数声明中不能直接使用 T&,因为实例化 TestPointerLike<void> 时,编译器会先形成函数签名,此时 T& 会变成非法的 void&。requires 可以让函数不参与候选,但无法修复已经非法的返回类型。
可以把返回类型写成 std::add_lvalue_reference_t<T>:
#include <concepts>
#include <type_traits>
template<typename T>
struct TestPointerLike {
T* m_data;
[[nodiscard]]
std::add_lvalue_reference_t<T>
operator[](std::integral auto i) const noexcept
requires (!std::is_void_v<T>)
{
return m_data[i];
}
};
int main()
{
TestPointerLike<void> p; // 合法,operator[] 不可用
}
对于普通对象类型,std::add_lvalue_reference_t<T> 等价于 T&。当 T 为 void 时,它的结果是 void,不会形成非法的 void&。requires (!std::is_void_v<T>) 则负责禁用这个成员函数。
为什么原来的 requires 没有生效
当 T = void 时,下面的声明仍需先确定返回类型:
T& operator[](std::integral auto i) const noexcept
requires (!std::same_as<std::remove_cvref_t<T>, void>);
实例化类模板时,T& 会立即变成:
void&
此时声明已经非法。编译器不会先检查 requires,然后再决定是否解析返回类型。约束用于判断函数是否可用以及参与重载决议,但不会推迟外层类模板参数在函数签名中的替换。
把声明和定义分开也无法解决,因为类定义中的函数声明仍然含有 T&:
T& operator[](...);
错误出现在处理函数声明时,与函数体写在哪里无关。
使用 decltype(auto) 推导返回类型
也可以让编译器根据函数体推导返回类型:
#include <concepts>
#include <type_traits>
template<typename T>
struct TestPointerLike {
T* m_data;
[[nodiscard]]
decltype(auto) operator[](std::integral auto i) const noexcept
requires (!std::is_void_v<T>)
{
return m_data[i];
}
};
int main()
{
TestPointerLike<void> p; // 合法
}
当 T 不是 void 时,表达式 m_data[i] 是左值。decltype(auto) 会保留它的引用属性,因此推导出的返回类型是 T&。
当 T = void 时,约束不满足,函数不可调用,也无需根据函数体推导返回类型。这里应使用 decltype(auto),因为普通 auto 通常会丢失引用:
auto operator[](...); // 通常推导为 T,丢失引用
decltype(auto) operator[](...); // 推导为 T&
验证函数是否被正确禁用
可以用依赖模板参数的 concept 检查:
#include <concepts>
#include <type_traits>
template<typename T>
struct TestPointerLike {
T* m_data;
[[nodiscard]]
std::add_lvalue_reference_t<T>
operator[](std::integral auto i) const noexcept
requires (!std::is_void_v<T>)
{
return m_data[i];
}
};
template<typename P>
concept Indexable = requires(P p) {
p[0];
};
template<typename P>
concept IntReferenceIndexable = requires(P p) {
{ p[0] } -> std::same_as<int&>;
};
static_assert(Indexable<TestPointerLike<int>>);
static_assert(IntReferenceIndexable<TestPointerLike<int>>);
static_assert(!Indexable<TestPointerLike<void>>);
该选择哪种写法
如果要在声明中明确写出返回类型,可以使用:
std::add_lvalue_reference_t<T>
如果返回类型适合由表达式决定,可以使用:
decltype(auto)
额外引入 U = T 也能工作。此时返回类型是依赖成员函数模板参数的 U&,要到函数模板替换阶段才会形成。不过在这个场景中,std::add_lvalue_reference_t<T> 和 decltype(auto) 更直接,也不必通过 std::same_as<T, U> 防止调用者显式指定其他类型。
如果必须在声明中保留原样的 T&,同时允许实例化 TestPointerLike<void>,仅添加 requires 无法做到。需要推迟返回类型的形成,或者为 void 提供单独的类模板偏特化。
备注:内容仅供参考。
