PAYATHON 2026

C++ 中如何仅在类中存在指定类型时定义 alias 和 getter/setter?

技术老齐

可以用类模板偏特化实现:主模板处理没有 MyStruct 的情况,偏特化版本定义 MyAlias 和对应的 getter/setter。整个方案不需要包含任何标准库头文件。

原代码中的 decltype((void)U::MyStruct, void()) 把 U::MyStruct 用作表达式,但这里的 MyStruct 是类型。应使用 typename U::MyStruct 检测类型,再通过自定义的 void_t 参与 SFINAE。

为什么没有选中偏特化

原来的检测条件是:

decltype((void)U::MyStruct, void())

其中,(void) 是转换为 void 的强制类型转换,后面需要一个表达式。这里把 U::MyStruct 当作表达式使用,无法检测同名的嵌套类型。

当 U 为 MyClass1 或 MyClass2 时,替换失败,偏特化无法匹配,编译器便使用主模板:

template<typename U, typename = void>
struct check_mystruct
{
    using type = void;
};

因此,MyAlias 最终成了 void。调用 doSomething() 时,编译器实例化函数体,其中的声明相当于下面这段不合法的代码:

void inputs {};

可以自行定义一个 void_t 来检测类型:

template<typename...>
struct make_void
{
    using type = void;
};

template<typename... Ts>
using void_t = typename make_void<Ts...>::type;

这只用到了 C++11 的语言功能,不依赖标准库。检测代码可以写成:

template<typename U, typename = void>
struct check_mystruct
{
    using type = void;
};

template<typename U>
struct check_mystruct<U, void_t<typename U::MyStruct>>
{
    using type = typename U::MyStruct;
};

如果 typename U::MyStruct 是合法类型,第二个模板实参就是 void,偏特化匹配成功;否则替换失败,编译器使用主模板。

这种写法仍会为所有接口定义 MyAlias,没有 MyStruct 时,MyAlias 就是 void。如果要求类型别名和访问函数都不存在,需要把它们一起放进类模板偏特化。

用偏特化按条件提供接口

所有接口都保留 doSomething()。没有 MyStruct 时,该函数执行空操作;有 MyStruct 时,接口还提供 MyAlias、getVar() 和 setVar()。

// Classes to interface with
class MyClass1
{
public:
    struct MyStruct
    {
        double f1;
        double f2;
    } _myVar {};
};

class MyClass2
{
public:
    struct MyStruct
    {
        double g1;
        double g2;
        double g3;
    } _myVar {};
};

class MyClass3
{
};

// No standard library required
template<typename...>
struct make_void
{
    using type = void;
};

template<typename... Ts>
using void_t = typename make_void<Ts...>::type;

// Primary template: no accessible nested type named MyStruct
template<typename T, typename = void>
class Interface
{
public:
    void doSomething()
    {
        // No MyStruct-related work to perform.
    }

private:
    T _object {};
};

// Partial specialization: T::MyStruct is a valid type
template<typename T>
class Interface<T, void_t<typename T::MyStruct>>
{
public:
    using MyAlias = typename T::MyStruct;

    const MyAlias& getVar() const
    {
        return _object._myVar;
    }

    void setVar(const MyAlias& newVar)
    {
        _object._myVar = newVar;
    }

    void doSomething()
    {
        MyAlias inputs {};
        // Add operations that use inputs here.
        (void)inputs;
    }

private:
    T _object {};
};

int main()
{
    Interface<MyClass1> i1;
    Interface<MyClass2> i2;
    Interface<MyClass3> i3;

    Interface<MyClass1>::MyAlias value1 {1.0, 2.0};
    Interface<MyClass2>::MyAlias value2 {3.0, 4.0, 5.0};

    i1.setVar(value1);
    i2.setVar(value2);

    const auto& result1 = i1.getVar();
    const auto& result2 = i2.getVar();
    (void)result1;
    (void)result2;

    i1.doSomething();
    i2.doSomething();
    i3.doSomething();

    // These would be compilation errors:
    // Interface<MyClass3>::MyAlias value3;
    // i3.getVar();
    // i3.setVar(value1);

    return 0;
}

Interface<MyClass3> 使用主模板,没有 MyAlias、getVar() 和 setVar()。尝试使用这些成员会产生编译错误。

如果没有 MyStruct 的类也不应提供 doSomething(),从主模板中删掉该函数即可。此时仍可实例化 Interface<MyClass3>,但不能调用 i3.doSomething()。

为什么成员函数模板加省略号不能解决问题

原来的写法是:

template<typename U = MyAlias>
void setVar(const U& newVar)
{
    _object._myVar = newVar;
}

void setVar(...)
{
    // Do nothing.
}

U = MyAlias 只是默认模板实参,并不限制 U 必须等于 MyAlias。普通调用时,编译器通常会从函数实参推导 U。例如:

i1.setVar(42);

这里会推导出 U = int。const int& 是合法的参数类型,函数模板可以参与重载决议,而且比省略号版本更匹配。

编译器选中函数模板并实例化函数体后,才会发现这条赋值语句不合法:

_object._myVar = newVar;

函数体中的这类错误不属于 SFINAE 的立即上下文,因此编译器不会回头改选省略号重载。

另外,setVar(...) 始终存在,不符合“没有对应类型时不提供 setter”的要求。它还会让部分错误调用静默地执行空操作,不利于接口检查。

使用时需要满足的条件

上述偏特化只检测 T::MyStruct 能否作为类型访问,不检测 _myVar 是否存在,也不检测赋值操作是否合法。

完整示例沿用了题目中生成类的约定:

  • T 可以通过 T _object {}; 初始化。
  • 存在可访问的 T::MyStruct 时,也存在公开的 _myVar,且其类型为 T::MyStruct。
  • MyStruct 支持示例中的默认初始化和赋值操作。

如果生成类不总是满足这些约定,就需要继续检测相应表达式,或为例外情况单独编写适配实现。

getVar() 返回内部成员的 const 引用,可以避免复制,调用方也无法通过该引用直接修改成员。接口对象销毁后,调用方不能继续使用这个引用。如果需要独立保存结果,可以将 getter 改为按值返回。

备注:内容仅供参考。