如何保留C++命名构造器语法并避免误作为成员方法调用?
我特别喜欢命名构造器(即返回自身类对象的静态方法)的语法,但经常不小心把它当成成员方法调用,导致对象没被正确修改。想找个既能保留这种语法,又能避免这类错误的办法。
我已经用了以as开头的命名规范(比如static Point Point::asPolar(...);),也考虑过把默认构造函数设为私有,但不确定这是不是最优解。
坚持用这种语法的原因:我总记不住自己定义的全局函数,不想把函数放在全局命名空间,用同名命名空间又觉得别扭,放在类的命名空间里用起来更顺手。
示例代码:
class TextureDef { public: TextureDef(){} // 默认是1x1x1、2D、r8等参数 static TextureDef as1D(uint32 xSize, TextureFormat::Type format = TextureFormat::rgba8, uint8 mipmapCount = 1); static TextureDef as2D(uint32 xSize, uint32 ySize, TextureFormat::Type format = TextureFormat::rgba8, uint8 mipmapCount = 1); static TextureDef as3D(uint32 xSize, uint32 ySize, uint32 zSize, TextureFormat::Type format = TextureFormat::rgba8, uint8 mipmapCount = 1); static TextureDef as1Darray(uint32 xSize, uint32 arrayCount, TextureFormat::Type format = TextureFormat::rgba8, uint8 mipmapCount = 1); static TextureDef as2Darray(uint32 xSize, uint32 ySize, uint32 arrayCount, TextureFormat::Type format = TextureFormat::rgba8,uint8 mipmapCount = 1); static TextureDef asBuffer(uint32 xSize, TextureFormat::Type format = TextureFormat::rgba8); static TextureDef asRect(uint32 xSize, uint32 ySize, TextureFormat::Type format = TextureFormat::rgba8); static TextureDef asCubemap(uint32 xySize, TextureFormat::Type format = TextureFormat::rgba8, uint8 mipmapCount = 1); static TextureDef asCubemapArray(uint32 xySize, uint32 arrayCount, TextureFormat::Type format = TextureFormat::rgba8, uint8 mipmapCount = 1); static TextureDef as2DMS(uint32 xSize, uint32 ySize, TextureFormat::Type format = TextureFormat::rgba8, uint8 samples = 4); static TextureDef as2DMSarray(uint32 xSize, uint32 ySize, uint32 arrayCount, TextureFormat::Type format = TextureFormat::rgba8, uint8 samples = 4); ... }; void usageExample(Canvas & canvas) { // 这种语法用着特别爽,毕竟我重度依赖自动补全 canvas.allocTexture(TextureDef::as2D(100, 100)); // 但我偶尔会犯蠢... TextureDef def; def.as2D(100, 100); // `def`根本没被修改 canvas.allocTexture(def); }
方案一:将默认构造设为私有,强制使用命名构造器
把默认构造函数改成私有,同时新增一个asDefault静态方法来创建默认参数的对象,从根源上避免创建空对象后误调用静态方法的情况。
修改后的类示例:
class TextureDef { private: TextureDef(){} // 默认构造私有 public: static TextureDef asDefault() { return TextureDef(); } // 替代默认构造 static TextureDef as1D(...); // 其他静态构造方法保持不变... // 如果需要修改已有的对象,可以加对应的成员方法,比如: TextureDef& setAs2D(uint32 xSize, uint32 ySize, ...) { // 这里修改当前对象的参数 return *this; } };
这样你就没办法直接TextureDef def;创建对象,必须用TextureDef def = TextureDef::asDefault();,从源头减少误操作的可能。而且专门的setXXX方法语义更清晰,不会和静态构造器混淆。
方案二:删除成员方法版本,编译期拦截错误调用
这种方法不需要改变现有静态构造器的写法,只需要在类里声明同名的成员方法并标记为delete,当你不小心用对象调用def.as2D()时,编译器会直接报错,把错误扼杀在编译阶段。
修改后的类示例:
class TextureDef { public: TextureDef(){} // 静态构造方法保持不变 static TextureDef as2D(uint32 xSize, uint32 ySize, TextureFormat::Type format = TextureFormat::rgba8, uint8 mipmapCount = 1); // 删除成员方法版本,禁止对象调用 void as2D(uint32 xSize, uint32 ySize, TextureFormat::Type format = TextureFormat::rgba8, uint8 mipmapCount = 1) = delete; // 其他asXXX方法同理,都加对应的delete成员方法 void as1D(...) = delete; void as3D(...) = delete; // ... };
这个方案完全保留了你喜欢的静态构造器语法,只要你误写成员调用,编译器就会抛出明确错误,比如GCC会提示error: use of deleted function 'void TextureDef::as2D(...)',直接阻止你犯这种错误。
方案三:重载成员方法并返回引用(可选,不推荐)
如果偶尔确实需要修改现有对象,可以给每个asXXX静态方法重载一个成员版本,让它修改当前对象并返回自身引用,这样即使误调用也能达到预期效果。但这种方法会让静态构造器和成员方法的语义混淆,长期来看可能增加理解成本,所以不推荐作为首选。
示例:
class TextureDef { public: TextureDef(){} static TextureDef as2D(uint32 xSize, uint32 ySize, ...) { TextureDef def; // 初始化def的参数 return def; } TextureDef& as2D(uint32 xSize, uint32 ySize, ...) { // 修改当前对象的参数 return *this; } };
这样def.as2D(100,100)会直接修改def对象,但缺点是静态方法和成员方法同名,容易让其他开发者困惑,也违背了命名构造器原本“创建新对象”的语义。
内容的提问来源于stack exchange,提问作者Anne Quinn

