C++20/23中如何避免引用类型下构造函数签名被求值
我希望当data_type是任意引用类型时,避免property类第一个构造函数的签名被求值——因为指向引用成员的指针是非法的(示例中name/set_name会触发该情况)。我原以为constexpr构造函数在name/set_name场景下不会被求值,但实际并非如此。我尝试过enable_if和requires语句,但都无法阻止签名本身被求值(事后看来这合理,因为需要求值签名来推导class_type和data_type)。请问有没有办法实现我的需求:在同一对象的constexpr上下文中,既能接受类成员并生成getter/setter的lambda,又能接受getter/setter对?
C++ 标准:20 或 23
test_property.cpp
#include <string> #include <tuple> #include "property.h" class object { public: int index() const { return this->_idx; } void set_index(int value) { this->_idx = value; } const std::string& name() const { return this->_name; } void set_name(const std::string& value) { this->_name = value; } private: bool _flag{}; int _idx{}; std::string _name{}; constexpr static auto properties = std::make_tuple( property("name", &object::name, &object::set_name), property("index", &object::index, &object::set_index), property("flag", &object::_flag) ); }; int main(int argc, char** argv) { object obj; return 0; }
property.h v1(使用enable_if)
#include <type_traits> template <typename class_type, typename data_type> class property { public: template <typename = std::enable_if_t<!std::is_reference_v<data_type>>> inline constexpr property(const char* name, data_type class_type::*property) { } inline constexpr property(const char* name, data_type (class_type::*getter)() const, void (class_type::*setter)(data_type)) { } };
property.h(使用concepts)
#include <type_traits> template <typename class_type, typename data_type> class property { public: inline constexpr property(const char* name, data_type class_type::*property) requires(!std::is_reference_v<data_type>){ } inline constexpr property(const char* name, data_type (class_type::*getter)() const, void (class_type::*setter)(data_type)) { } };
核心问题在于:property类模板的class_type和data_type是构造函数匹配前就必须确定的,导致第一个构造函数的data_type class_type::*签名会被强制求值——哪怕加了requires或enable_if,编译器推导模板参数时就会先解析这个签名,而指向引用的成员指针本身就是语法错误,直接触发报错。
要解决这个问题,需要把构造函数的参数推导和类模板参数解耦,以下是两种可行方案:
方案1:工厂函数+类特化
用外层工厂函数负责参数推导,再根据推导结果选择对应的property特化版本,避免类模板参数提前固定:
#include <type_traits> #include <utility> // 前置声明 template <typename ClassType, typename DataType> class property; // 工厂函数:处理成员变量(仅当DataType非引用时启用) template <typename ClassType, typename DataType> constexpr auto make_property(const char* name, DataType ClassType::*mem_ptr) requires (!std::is_reference_v<DataType>) { return property<ClassType, DataType>(name, mem_ptr); } // 工厂函数:处理getter/setter对 template <typename ClassType, typename DataType> constexpr auto make_property(const char* name, DataType (ClassType::*getter)() const, void (ClassType::*setter)(DataType)) { return property<ClassType, DataType>(name, getter, setter); } // 主模板:处理getter/setter对的情况 template <typename ClassType, typename DataType> class property { public: using getter_t = DataType (ClassType::*)() const; using setter_t = void (ClassType::*)(DataType); constexpr property(const char* name, getter_t getter, setter_t setter) : _name(name), _getter(getter), _setter(setter) {} constexpr DataType get(const ClassType& obj) const { return (obj.*_getter)(); } constexpr void set(ClassType& obj, DataType value) const { (obj.*_setter)(std::move(value)); } private: const char* _name; getter_t _getter; setter_t _setter; }; // 特化模板:处理成员变量,自动生成getter/setter template <typename ClassType, typename DataType> class property<ClassType, DataType> requires (!std::is_reference_v<DataType>) { public: using mem_ptr_t = DataType ClassType::*; constexpr property(const char* name, mem_ptr_t mem_ptr) : _name(name), _mem_ptr(mem_ptr) {} constexpr DataType get(const ClassType& obj) const { return obj.*_mem_ptr; } constexpr void set(ClassType& obj, DataType value) const { obj.*_mem_ptr = std::move(value); } private: const char* _name; mem_ptr_t _mem_ptr; };
使用时把property替换为make_property即可:
constexpr static auto properties = std::make_tuple( make_property("name", &object::name, &object::set_name), make_property("index", &object::index, &object::set_index), make_property("flag", &object::_flag) );
方案2:模板构造函数延迟推导
把property的构造函数改为模板,让参数推导延迟到构造函数内部,避免类模板参数提前固定导致的签名求值:
#include <type_traits> #include <utility> template <typename ClassType, typename DataType> class property { public: // 模板构造函数:仅处理成员变量(DataType非引用时启用) template <typename MemType> constexpr property(const char* name, MemType ClassType::*mem_ptr) requires std::is_same_v<std::remove_cv_t<MemType>, std::remove_cv_t<DataType>> && !std::is_reference_v<DataType> { // 存储成员指针,生成lambda形式的getter/setter _getter = [mem_ptr](const ClassType& obj) -> DataType { return obj.*mem_ptr; }; _setter = [mem_ptr](ClassType& obj, DataType value) { obj.*mem_ptr = std::move(value); }; } // 构造函数:处理getter/setter对 constexpr property(const char* name, DataType (ClassType::*getter)() const, void (ClassType::*setter)(DataType)) { _getter = [getter](const ClassType& obj) -> DataType { return (obj.*getter)(); }; _setter = [setter](ClassType& obj, DataType value) { (obj.*setter)(std::move(value)); }; } // 统一的get/set接口 constexpr DataType get(const ClassType& obj) const { return _getter(obj); } constexpr void set(ClassType& obj, DataType value) const { _setter(obj, std::move(value)); } private: // C++23可直接用std::function,C++20需用自定义可调用类型或变体存储 auto _getter; auto _setter; };
这种方式下,传入成员函数指针时,编译器会直接匹配第二个构造函数,不会尝试解析第一个构造函数的成员指针签名,从根源避免了引用成员指针的语法错误。
内容的提问来源于stack exchange,提问作者fauxanadu

