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 改为按值返回。
备注:内容仅供参考。
