PAYATHON 2026

如何在 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 提供单独的类模板偏特化。

备注:内容仅供参考。