支付宝签约网站,加强红色网站建设,如何做求婚网站,wordpress 博客二号下兵营、弹药库和指挥所等设施#xff0c;有些地下工事深达数十米。然而#xff0c;在1940年的法国战役中#xff0c;马奇诺防线并未发挥预期作用#xff0c;德军绕过马奇诺防线#xff0c;通过比利时和阿登森林发动突袭#xff0c;迅速击败了法军。这使得马奇诺防线成为…下兵营、弹药库和指挥所等设施有些地下工事深达数十米。然而在1940年的法国战役中马奇诺防线并未发挥预期作用德军绕过马奇诺防线通过比利时和阿登森林发动突袭迅速击败了法军。这使得马奇诺防线成为了过时防御思维的象征。代码中的防御日常工作中编写一些功能类或者一个相对完备的功能模块是比较常有的我们编写它们要么是供其他同事使用要么作为积累和展示。一旦想到自己的代码可能被其他同行调用我们就会一改平时漫不经心的态度幻想着他人会触碰我们代码中的逻辑缺陷败坏他们的程序自己落得颜面扫地。于是我们步步为营每添加或修改一点功能就会仔细推敲有可能发生的意外运用自己掌握的手段攘除它们滋生的可能保证我们的代码永远行驶在正轨之上这就像代码中的防御工事。但是这种防御与网络和数据安全领域的防御又有所区别后者的防御对象经常是那些图谋不轨者就像战争中的敌人一样与我们是对立的。而我们的这种防御更像是一种辅助它让我们的用户尽可能不陷入困境就像 Effective C 中说的让接口更难被误用。一个例子假设我们有一个比较复杂的类 Complex它有诸多成员变量其中某些成员组合在一起可以描述一个抽象的属性多个属性可能会涉及相同的成员变量将与某个属性关联的成员打包到一个属性类中是不可行的。一个解决方案是将各种的抽象属性封装为不同的代理类它们各自只引用自己关心的 Complex 数据成员通过这些代理类来读取或修改相应的属性思路如图一个确切的例子假如我们有一个房子它的内部包含多种家用电器现在我们想为这个房子的智能控制系统编写一系列的模式比如归家模式、电影模式、度假模式等等。我们就可以将这些模式包装为多个类各个模式只引用它们需要操作的电器用以查询或改变房子的状态。假如我们编写了如下代码代码class House;class MovieMode{public:enum class Atmosphere{Romantic,Action,Horror,Drama,//其他氛围...};bool is_on() const;//电影模式是否开启void turn_on();//开启电影模式Atmosphere get_atmosphere() const;//获取观影氛围void set_atmosphere(Atmosphere ap);//设置观影氛围//其他接口...//明确表明不希望任何形式的复制MovieMode(const MovieMode ) delete;MovieMode operator(const MovieMode ) delete;MovieMode operator(MovieMode ) noexcept delete;private:MovieMode(House house, Light mood_light, Device _projector,Device stereo_system);//用于从函数返回MovieMode(MovieMode ) noexcept default;private:House _house;//操作的对象Light _mood_light;//氛围灯Device _projector;//投影仪Device _stereo_system;//音响//其他可能关联的设备...//将House声明为友元使它能够创建和返回本类的实例friend class House;};class House{public://构造、设置等接口...//打开所有灯void turn_on_all_lights();//关闭所有灯void turn_off_all_lights();//打开所有电器void turn_on_all_devices();//关闭所有电器void turn_off_all_devices();//创建电影模式MovieMode movie_mode();const MovieMode movie_mode() const;//创建其他模式...private://各种灯具Light _entry_light;//玄关灯Light _corridor_light;//过道灯Light _mood_light;//氛围灯Light _kitchen_light;//厨房灯//其他的灯具...//各种电器Device _refrigerator;//电冰箱Device _projector;//投影仪Device _stereo_system;//音响Device _air_filter;//空气净化器//其他电器...};这段代码显得有些粗制滥造实际情况中应该为各种家用电器设计一套继承体系方便分类与管理房子也可以分割为不同的房间每个家用电器可以放置到不同的房间内。但是作为例子用以说明本文的意图足矣。可以看到MovieMode 类除定义了与功能相关的接口外其构造函数、拷贝控制系列函数都做了显式定义。而 House 类创建 MovieMode 的接口区分 const 和非 const 两个版本。这样的定义主要有以下考量1. MovieMode 构造函数和移动构造函数为私有只能由友元 House 类的接口创建它的实例和从函数中返回代码MovieMode House::movie_mode(){return MovieMode(*this, _mood_light, _projector, _stereo_system);}const MovieMode House::movie_mode() const{return const_cast(this)-movie_mode();}2. MovieMode 的拷贝构造、拷贝赋值、移动赋值函数皆显式删除除通过 House 的实例获取外用户无法以其他方式创建或拷贝 MovieMode 的实例代码House house;house.movie_mode().turn_on();house.movie_mode().set_atmosphere(MovieMode::Atmosphere::Romantic);MovieMode mode house.movie_mode();//错误无法拷贝或移动构造 MovieMode这点很重要因为 MovieMode 是一个代理类它的内部保存着一个对 House 对象的引用所以它的生命周期必须是它所绑定的 House 对象的子集否则对它的读写操作就可能是在操作一个空悬引用。3. House 创建 MovieMode 的接口区分 const 和非 const 版本这保证了 const House 实例不会被意外修改代码const House chouse;MovieMode::Atmosphere ap chouse.movie_mode().get_atmosphere();chouse.movie_mode().turn_on();//错误无法通过 const MovieMode 对象调用经过测试用例的验证这一系列防御措施确实可以防止预想中各种意外情况的发生于是我们满怀信心地将这份代码提供了出去。防御失效假如经过一段时间后我们的这个模块被其他人引用但是我们发现了这样的代码让我们后背一凉代码const House chouse;auto mode chouse.movie_mode();mode.set_atmosphere(MovieMode::Atmosphere::Romantic);后两句代码直接无视了我们精心设下的防御工事既“复制”了 MovieMode 对象还通过它修改了一个 const House 对象的状态这让我们有点惊讶我们在测试代码时这样的语句已经确定是无法通过编译的但是现在它们活生生的在我们眼前编译器没有一点阻挠地编译成功了。经过一番对比后我们恍然大悟这些代码是使用 C17 语言标准编译而我们测试的环境是 C14。读到此处有的读者可能会想C26 都快发布了为什么还用 C14 在作者所处的传统软件工作环境中C17 及之后的版本普及率确实相当低很多项目甚至还在用 C11像作者这种新手才会好奇地摆弄新的语言特性。我们都知道 C 的拷贝省略Copy Elision而在这篇博客里我正好已经讨论过它与语义检查的关系。C14 标准下语义检查会拒绝上述代码的写法因为它引用了无法访问的移动构造函数而到了 C17 中由于强制拷贝省略要求这种情形下必须省略移动构造函数的调用编译器只需检查在 mode 真正的构造函数调用处movie_mode 的函数内部更具体地说是非 const 版本的 House 的 movie_mode 内部MovieMode 的构造函数可访问即可而由于 House 类是 MovieMode 类的友元在它的成员函数内构造 MovieMode 对象当然是没问题的。auto mode chouse.movie_mode(); 这一句的语义是从一个函数返回的临时的 const MovieMode 对象构建一个非 const 的 MovieMode 对象 mode需要经历一次构造多次移动和移动构造然而在强制拷贝省略的前提下只需在真正的创建点原位构造一个非 const 的 MovieMode 对象即可所以上述代码可成功编译。就像马奇诺防线一样我们的防御失效了新的语言标准可以让危险代码绕过我们坚固的防御工事直击要害。这让我想起以前玩植物大战僵尸时的情形我布置了强大的地面火力和结实的高坚果墙但是突然出现几只气球僵尸大摇大摆地飞进房子吃掉了脑子。不过幸好这不像战争一样只有一次机会我们可以对我们的防御工事进行补救。方案一成员函数引用限定符强制拷贝省略会直接无视我们的防御措施所倚仗的流程那么有没有办法让强制拷贝省略失效查阅了一些资料看到说在返回临时对象的函数内增加一些分支逻辑可以干扰编译器对于强制拷贝省略可行性的判断这样我们可以牺牲一点点效率来重新获取安全性比如代码MovieMode House::movie_mode(){uintptr_t addr (uintptr_t)this;MovieMode *pm nullptr;if(addr 0x1000)//一个运行时才能确定的判断return MovieMode(*this, _mood_light, _projector, _stereo_system);else{MovieMode mode(*this, _mood_light, _projector, _stereo_system);pm mode;return std::move(*pm);}}上述代码为了制造一些干扰已经非常刻意了然而可惜的是使用 MSVC、clang、GCC 对这段代码进行编译在语言标准设定为 C17 的情况下auto mode chouse.movie_mode(); 这一句代码全都顺利通过编译看来编译器在这方面都是非常激进的。不过就算这样能够成功阻止编译器施行强制拷贝省略我也不打算编写这样丑陋不堪的代码。既然几乎无法阻止独立的 MovieMode 被构造出来我们就要在其他地方想办法了。有很长一段时间我都想不出什么合适的方法来直到一次翻 Effective Modern C 时在条款十二Declare overriding functions override中看到成员函数的引用限定符这个 C11 添加的语言特性。这个概念在我最开始读 C Primer 时就知晓但是以当时我的代码经历来说这个特性毫无用武之地之后的开发中也没有用过它所以渐渐淡忘了再次看到它时我是非常激动的它就是我寻找的东西真是 “初闻不识曲中意再听已是曲中人” 啊。考虑到:代码House house;house.movie_mode().set_atmosphere(...);//set_atmosphere 是在一个右值上调用的auto mode house.movie_mode();mode.set_atmosphere(...);//set_atmosphere 是在一个左值上调用的添加一个右值引用限定符我们就能限制 set_atmosphere 只能通过 MovieMode 的右值对象调用了代码class MovieMode{public:void set_atmosphere(Atmosphere ap) //右值引用限定//其他成员...};House house;house.movie_mode().set_atmosphere(...);//OKauto mode house.movie_mode();mode.set_atmosphere(...);//错误不能在 lvalue 上调用 MovieMode::set_atmosphere现在就算用户得到一个独立的 MovieMode 对象也无法用它来调用 set_atmosphere 了。同理我们可以为 MovieMode 的其他接口都添加右值引用限定const 属性仍保持原样这样就算用户通过某种方法获得了一个空悬的 MovieMode 对象他也没法在其上做出进一步的操作而引发未定义行为了。方案二依赖稳定的语言特性上述诸多麻烦的根本原因是我们的特性依赖了一个不是长期保持不变的语义一旦这个语义发生根本性的改变我们就必须做出调整。我们知道从一个非临时的对象构造另一个对象需要调用拷贝构造函数这一语言规则是一定不会被改变的我们的 House 类可以将控制系统支持的所有模式作为成员存储在类内只返回它们的引用代码class House;class MovieMode{public://明确表明不希望任何形式的复制MovieMode(const MovieMode ) delete;MovieMode operator(const MovieMode ) delete;MovieMode(MovieMode ) noexcept delete;MovieMode operator(MovieMode ) noexcept delete;//其他成员函数...private:MovieMode(House house, Light mood_light, Device _projector,Device stereo_system);friend class House;//其他成员...};class House{public:MovieMode movie_mode() { return _movie_mode; }const MovieMode MovieMode() const { return _movie_mode; }//其他成员函数...private:MovieMode _movie_mode;//其他成员...};现在用户连创建一个独立的 MovieMode 变量都不可能了操作某个 House 实例的 MovieMode 必须通过 movie_mode 接口调用来完成而且这样的设计可以确保长期的稳定性因为它所依赖的语言特性是 C 对象模型最基础的规则之一几乎不可能变更。当然这种实现的代价是大大增加了 House 类的内存占用如果实际应用中需要创建非常多的 House 实例这种方式可能也不是最佳选择。刻意的破坏不在防御目标之列有人说仍然有方法可以绕开限制代码//针对方案一auto create_dangling_mode() - decltype(std::declval().movie_mode()){return House().movie_mode();}create_dangling_mode().set_atmosphere(...);//在空悬的 MovieMode 调用 set_atmosphereconst House chouse;auto mode chouse.movie_mode();static_cast(mode).set_atmosphere(...);//修改 const 对象的数据//针对方案二const House chouse;const_cast(chouse.movie_mode()).set_atmosphere(...);//修改 const 对象的数据这些并不是什么高超的编程技巧我们确实无法也没有义务防御这样的操作。就像是坐飞机时非要解开安全带撬开舷窗把头伸出窗外一样我们只能说自作孽不可活祝他好运。为易用性而妥协上面的两种修改方案都能够保证正常用户只能通过这样的方式去调用 MovieMode 的接口代码House house;house.movie_mode().xxx();这样能够保证每个创建出来的 MovieMode 的生命周期都是它所绑定的 House 对象的子集不会出现空悬引用。但是正是最开始 auto mode house.movie_mode(); 这句代码让我们产生了思考用户一般不会故意去测试代码的边界情况那么站到用户的角度来看为什么会这样编写呢考虑到这样使用场景代码中有多个条件分支都要对同一个 House 的 MovieMode 进行设置代码House house;if(...)house.movie_mode().xxx();else if(...)house.movie_mode().xxx();//还有许多分支这种情况下重复地键入 house.movie_mode() 可能形成不好的体验而这样的编写方式会更方便自然代码House house;auto mode house.movie_mode();if(...)mode.xxx();else if(...)mode.xxx();//还有许多分支如果每次设置都需要通过调用一长串的函数来实现确实会影响使用体验我们或许应该为易用性而妥协。文章开头已经说到我们与用户之间并不是你死我活的敌对关系我们可以在接口文档中提醒用户哪些使用方式是危险的需要避免相信用户不会故意去违背这些善意的提示编写危险的代码。假如我们在方案一中去掉了 MovieMode 成员函数上施加的右值引用限定符或者在方案二中解除了 MovieMode 不可复制的限制为易用性让路并撰写了详细的说明文档用户也非常配合然而开始的那个会意外修改 const 数据的问题会再次浮现出来代码const House chouse;auto mode chouse.movie_mode();mode.xxx();//mode 不是 const 的可能意外修改一个 const 对象的数据创建的 mode 是一个非 const 的版本通过它能够修改 chouse 的数据。而要求用户注意到这一点每次调用一个 const 版本的 House 实例的 movie_mode 都写成: const auto mode chouse.movie_mode(); 也是不切实际的这个问题是我们必须解决的。解决方法也很简单MSVC 标准库 std::vector 的迭代器iterator设计已经给了我们示范iterator 继承自 const_iteratorconst_iterator 实现读操作iterator 实现写操作。vector iterator我们的 MovieMode 跟迭代器非常相似完全可以采用相同的设计方式代码class Const_MovieMode; //所有的读操作class MovieMode : public Const_MovieMode//所有的写操作class House{public:Const_MovieMode movie_mode() const;MovieMode movie_mode();};现在用户创建一个 const 版本的 House 然后通过 movie_mode 得到的是一个 Const_MovieMode在这个对象上是绝对无法修改绑定对象的数据的。或许有能够兼顾安全性和易用性的实现毕竟在 C 中你几乎总能够找到方案实现你的任何想法但是就我目前的水平来看已经捉襟见肘了还须继续学习。总结1. C 中一旦你想构建一个与默认行为相异的类你就不得不精细定制类的各种行为以达到你的设计目的而这种定制大概率会改变一些你未注意到的行为为此你不得不深究相关的语言细节而当你了解的更深后你又会回过头发现之前的实现可以重构重构的过程中又会触碰更多细节如此往复。很多人说 C 是一门心智负担很重的语言但是在其他的编程语言中这种情形同样存在它是学习过程的必然现象不过 C 在这一点上表现得尤为突出。