Effective Modern C++: Item 7 -> 创建对象时分清()和{}

本文探讨了C++11中引入的大括号初始化语法。它不仅用途广泛,还能防止隐式窄化转换,并且对C++的most vexing parse免疫。然而,在构造函数重载决议中,大括号初始化倾向于匹配std::initializer_list参数版本的构造函数。文章还讨论了大括号和小括号在模板和std::vector中的使用差异。

当说到大名鼎鼎的特性,C++11和C++14有许多可以吹。auto,智能指针,移动语义,lambda表达式,并行—-每一个都是如此重要,值得我用一个章节来阐述。掌握这些特性很重要,但是成为一名有效率的现代C++程序员也需要走过一系列更小的步骤。每一步回答一个专门的问题,这些问题是从C++98转向现在C++11过程会出现的。当你创建对象时,什么时候应该使用大括号而不是小括号?为什么别名声明要比typedef要好?constexpr和const有什么区别?const成员函数和线程安全之间有什么关系?这份列表还在继续。本章将一个一个的提供解答。

创建对象时分清()和{}

取决于你的观点,C++11中的对象初始化语法选择要么就表现地非常不堪,要么就非常令人迷惑。作为一个通用规则,指定初始化值可以使用大括号,等号或者小括号。

int x(0);       //initializer is in parentheses

int y = 0;      //initializer follows "="

int z{0};       //initializer is in braces

在许多情况下,也可以共同使用一个等于号和大括号:

int z = {0};    //initializer uses "=" and braces

对于本条款后面的部分,我会忽略这种等号+大括号的语法形式,因为C++对其的处理方式一般和只有大括号那种一样。

迷惑方指出使用一个等号来初始化经常误导C++新手,让他们误认为这是赋值在其作用,即使那并不是。对于内建类型如int,区别很理论化,但是对于用户自定义类型,能够区分初始化和赋值就很重要,因为不同的函数会被涉及到:

Widget w1;      //call default constructor

Widget w2 = w1; //not an assignment; call copy ctor

w1 = w2;        //an assignment; call copy operator=

即使在一些初始化语法上,也有一些情况下,C++98是没法表达出一个想要的初始化的。例如,不可能直接指示一个STL容器被创建时应该持有一个专门的值的集合(比如1,3,5)。

为了解决多个初始化语法的困惑,同时也基于它们并没有涵盖所有的初始化场景,C++11引入了统一初始化(uniform initialization):一个单一的初始化语法,但是可以,至少在概念上可以,被用在任何地方,表达任何事物。它基于大括号,而基于这个原因我更喜欢称之为大括号初始化。“统一初始化”是一个主意,“大括号初始化”则是一个语法结构。

大括号初始化能够让你表达出之前无法表达式东西。使用大括号,指定一个容器的初始化内容就很容易:

std::vector<int> v{1,3,5};  //v's initial content is 1,3,5

大括号也能够被用来指定non-static数据成员的默认初始化值。这一能力—对C++11来说是全新的—也被用于等号初始化语法上,但没有包括小括号:

class Widget{
    ...
    private:
        int x{0};//fine,x's default value is 0
        int y = 0;//also fine
        int z(0);//error!
};

另一方面,不可拷贝对象(如std::atomic —参考Item 40)可以使用大括号或者小括号进行初始化,但是不能用等号:

std::atomic<int> ai1{0};//fine
std::atomic<int> ai2(0);//fine
std::atomic<int> ai3 = 0;//error

很容易理解为什么大括号初始化被称为“统一初始化”。在C++三种初始化表达式的方式中,只有大括号可以用于所有地方。

大括号初始化的新奇特性就是它不允许内置类型间的隐性收缩转换(implicit narrowing conversions)。如果大括号初始化里面的表达式的值不能够被要被初始化的对象类型所表达,那么该代码不能够通过编译:

double x,y,z;
...
int sum1{x+y+z};//error!sum of double may not be expressible as int

而使用小括号和等号进行的初始化则不检查收缩转换,因为这样可能会破坏太多目前合法的代码:

int sum2(x+y+z);    //ok (value of expression truncated to as int)
int sum3 = x+y+z; //ditto

大括号初始化另一个值得注意的特性就是它对于C++的most vexing parse免疫。C++规则的一个副作用就是任何可以被解析成声明的东西一定会被翻译成声明,most vexing parse最常影响开发者的是当他们想要默认构造一个对象时,但是最终却以声明一个函数告终。问题的根源在于如果你想要调用一个只有一个参数的构造器,你可以像这样写:

Widget w1(10);  //call Widget ctor with argument 10

但是如果你尝试使用相同的语法来调用Widget没有参数的构造器,你其实是声明了一个函数,而不是一个对象:

Widget w2();    //most vexing parse!declares a function
                //named w2 that returns a Widget

而函数是不能用大括号来包含参数列表进行初始化的,所以使用大括号来默认构造一个对象就没有上面的问题:

Widget w3{};    //calls Widget ctor with no args

所以对于大括号初始化其实有很多可以说的。这种语法可以被用于最广泛的上下文中,它防止了隐性收缩变换,并且它对于C++的most vexing parse免疫。三连胜式的好处!所以为什么本条款的名字不取成类似“完美的大括号初始化语法”这种?

大括号初始化的缺点是伴随它的有时候出现的令人吃惊行为。这种行为是由大括号初始化表达式,std::initializer_list和构造函数的重载决议之间错综复杂的关系造成的。它们之间的互动会导致代码看起来像是应该做这件事,但实际上做的确实另一件事。例如,Item 2解释了当一个auto声明的变量使用了大括号初始化表达式,那么推断出来的类型就是std::initializer_list,尽管按照其他声明方式来使用相同的初始化表达式会得到一个更加直观的类型。结果就是,你越喜欢用auto,你可能就越不对大括号初始化感兴趣。

在构造函数的调用中,小括号和大括号有着相同的含义,只要std::initializer_list参数没有涉及到:

class Widget{
public:
    Widget(int i, bool b);       //ctor not declaring
    Widget(int i, double d);     //std::initializer_list params
    ...
};

Widget w1(10,true);       //calls first ctor
Widget w2{10,true};       //also calls first ctor
Widget w3(10,5.0);        //calls second ctor
Widget w4{10,5.0};        //also calls second ctor

然而,如果一个或多个构造函数声明了std::initializer_list类型的参数,那么使用大括号初始化的调用则更加倾向于使用接受std::initializer_list参数的重载版本。非常强烈!如果编译器能够用接受std::initializer_list的构造函数来表达使用大括号初始化的调用,那么编译器肯定会选择这种方式。比如,如果上面的Widget类再加上一个接受std::initializer_list的构造函数:

class Widget{
public:
    Widget(int i, bool b);//as before
    Widget(int i, doubel d);//as before

    Widget(std::initializer_list<long double> il);//added
    ...
}

那么Widget对象w2和w4就要使用新的构造函数来构造了,尽管std::initializer_list中元素类型是long double,跟非std::initializer_list的构造函数相比,其对于两个参数的匹配程度都更差了!看:

Widget w1(10,true);//uses parens and ,as before,
                    //calls first ctor
Widget w2{10,true};//uses braces, but now calls
                    //std::initializer_list ctor
                    //(10 and true convert to long double)
Widget w3(10,5.0);//uses parens and ,as before,
                    //calls second ctor
Widget w4{10,5.0};//uses braces,but now calls
                    //std::initializer_list ctor
                    //(10 and 5.0 convert to long double)

即使是正常情况下应该使用的复制和移动构造函数也可能被std::initializer_list构造器劫持:

class Widget{
public:
    Widget(int i,bool b);  //as before
    Widget(int i,double d);//as before
    Widget(std::initializer_list<long double> il); //as before
    operator float() const;   //convert to float
    ...
};

Widget w5(w4);  //uses parens, call copy ctor
Widget w6{w4};  //uses braces, calls
                //std::initializer_list ctor
                //(w4 converts to float, and float converts to long double)
Widget w7(std::move(w4));//uses parens,call move ctor
Widget w8{std::move(w4)};//uses braces,calls
                        //std::initializer_list ctor
                        //(for same reason as w6)

编译器对于将使用大括号初始化与接受std::initializer_list的构造函数进行匹配的决定非常强烈,即使这最佳匹配的std::initializer_list构造函数不能被调用,它也坚持调用它。例如:

class Widget{
public:
    Widget(int i,bool b);// as before
    Widget(int i,double d);//as before

    Widget(std::initializer_list<bool> il);//element type is now bool
    ...                                    //no implicit conversion funcs
};

Widget w{10,5.0};   //error!requires narrowing conversions

这里,编译器会忽略前两个构造函数(第二个其实和那两个参数类型完全匹配)并且尝试调用接受std::initializer_list<bool>的构造函数。调用该构造函数需要将int(10)和double(5.0)转换成bool。这两个转换都是收缩转换(bool不能准确的表达出其中任意一个的值),但是收缩转换在大括号初始化中是禁止的,所以这个调用是非法的,代码编译报错。

只有当编译器没有办法将大括号初始化表达式中的参数类型转换成std::initializer_list中的类型,它才会按照正常的重载决议来选择重载函数。例如,如果我们将std::initializer_list<bool>构造函数替换成std::initializer_list<std::string>构造函数,那么non-std::initializer_list的构造函数就又称为候选成员啦,因为是没有办法将int和bool转换成std::string类型的。

class Widget{
public:
    Widget(int i,bool b);// as before
    Widget(int i,double d);//as before

    //std::initializer_list element type is now std::string
    Widget(std::initializer_list<std::string> il);
    ...                                    //no implicit conversion funcs
};

Widget w1(10,true); //uses parens,still calls first ctor
Widget w2{10,true;} //uses braces,now calls first ctor
Widget w3(10,5.0);  //uses parens ,still callls second ctor
Widget w4{10,5.0};  //uses braces,now calls second ctor

我们对于大括号初始化和构造函数重载的说明到这差不多就快结束了,但是有一个有趣的边缘case需要注意。假设你使用一个空的大括号来构造一个对象,这种方式既支持默认的构造函数,又支持std::initializer_list构造函数。那么这个空大括号到底意味着什么?如果它们表示”没有参数”,那么应该调用默认构造函数,但是如果它们表示”空的std::initializer_list”,那么应该调用std::initializer_list构造函数。

规则指定应该调用默认构造函数。空的大括号表示没有参数,而不是一个空的std::initializer_list:

class Widget {
public:
    Widget(); // default ctor
    Widget(std::initializer_list<int> il);  // std::initializer_list ctor// no implicit conversion funcs
};                                         
Widget w1; // calls default ctor
Widget w2{}; // also calls default ctor
Widget w3(); // most vexing parse! declares a function!

如果你想要调用参数为空的std::initializer_list的std::initializer_list构造函数,你可以在构造函数参数里面这么做–将空的大括号放到大括号或者小括号里面:

Widget w4({});          //calls std::initializer_list ctor with empty list

Widget w5{{}};          //ditto

这个时候,随着大括号初始化表达式,std::initializer_list和构造函数重载之间晦涩难懂的规则在你脑海中旋转,你可能想知道这部分信息跟你平时编程到底有多大关系。比你想象的还要大,因为一个直接受其影响的类就是std::vector。std::vector有一个non-std::initializer_list的构造函数,其允许你指定容器的初始大小和每一个元素的初始值,但是std::vector还有一个接受std::initializer_list的构造函数,允许你指定容器中的初始值。如果你创建了一个数值类型的std::vector(比如说一个std::vector<int>)并且传递了两个参数进去,你选择使用大括号还是小括号将会产生巨大的不同:

std::vector<int> v1(10,20);//use non-std::initializer_list ctor: 
                            //create 10-elemtns std::vector
                            //all elements have value of 20
std::vector<int> v2{10,20};//use std::initializer_list ctor:
                            //create 2-element std::vector,
                            //element values are 10 and 20

但是咱们从std::vector和大括号,小括号以及构造函数重载决议规则中跳回来。上面的讨论中有两点主要收获。第一,作为一个类作者,你需要意识到如果你的重载构造函数中包含一个或几个接受std::initializer_list的函数,那么使用大括号初始化表达式的客户代码可能只能看到std::initializer_list的重载构造函数。作为结果,最好设计你的构造函数,使得重载调用不会被客户使用大括号还是小括号影响。换句话说,你应该从std::vector中学习,虽然这些在现在看来可能是错误的,然后设计你的类去避免它。

从上面得到的一个启发就是如果你有一个类,它没有std::initializer_list构造函数,然后你加了一个,那么使用大括号初始化的客户代码可能发现以前决议成non-std::initializer_list构造函数的调用现在决议成了新函数。当然,当你在一堆重载函数中增加一个新函数,这种事情随时都可能发生:以前决议成一个老的重载函数的调用现在可能开始调用新的函数了。std::initializer_list构造函数重载不同之处在于,它不是单纯的和其他重载进行竞争,而是它的存在会把其他重载置于一种编译器基本上都不会考虑的境地。所以,增加这种重载真的需要深思熟虑。

第二,作为一个类的使用者,你在创建对象的时候必须在大括号和小括号之间认真选择。大部分开发者一般就选择一种作为默认,只有在不得已的时候才使用另外一种。默认使用大括号的那群人被大括号无敌的适用性,对收缩转换的禁止性以及对C++ most vexing parse的免疫性所吸引。这群人明白在一些case里(比如创建一个给定大小和初始值的std::vector时),小括号是必须的。另一方面,小括号的拥护者们则被它与C++98语法传统的一致性,其对于auto关于std::initializer_list的类型推断导致的问题的免疫性,还有当创建对象时其构造函数不会无意被std::initializer_list构造函数伏击等事实所吸引。他们承认有些时候只有大括号可以做到(比如创建一个拥有 特定值的容器)。谁更好其实并没有一个定论,我的建议就是选择一个并且坚持使用。

如果你是一个模板作者,大括号和小括号之间的关系就紧张到白热化了,因为,一般来说,根本没法知道哪一个会被使用到。例如,假设你想创建一个任意类型的接收任意数量参数的对象,一个可变模板让这一切变得直观:

template<typename T,    //type of object to create 
    typename... Ts>     //types of arguments to use
void doSomeWork(Ts&&... params)
{
    create local T object from params...
    ...
}

有两种方法可以将上面的伪代码转换成真实代码(关于std::forward请参考Item 25)

T localObject(std::forward<Ts>(params)...);//use parens
T localObject{std::forward<Ts>(params)...};//use braces

所以考虑下面的调用代码:

std::vector<int> v;
...
doSomeWork<std::vector<int>>(10,20);

如果doSomeWork在创建localObject的时候使用的是小括号,那么结果就是std::vector有10个元素。而如果doSomeWork使用的是大括号,则结果就是std::vector只有2个元素。哪一个是正确的?doSomeWork的作者没法知道。只有调用者才知道。

而这正是标准库函数std::make_unique和std::make_shared(见Item 21)所面临的问题。这些函数通过内部使用小括号并且将该决定记录下来作为其接口的一部分来解决这个问题。

要点记忆

  • 大括号初始化语法是用处范围最广的初始化语法,它可以防止收缩转换并且对C++的most vexing parse免疫
  • 在构造函数重载决议中,大括号初始化会尽一切可能匹配std::initializer_list参数版本的构造函数,即使其他构造函数似乎看起来更加匹配
  • 一个使用大括号和小括号会造成巨大差异的例子就是创建一个有两个参数的std::vector<数值类型>的对象
  • 在模板内的对象创建过程中选择大括号还是小括号是很有挑战性的。
内容概要:本文围绕“考虑电能交互的冷热电区域多微网系统双层多场景协同优化配置”的Matlab代码实现展开,提出一种结合电能交互机制的双层优化模型,用于解决冷、热、电多能耦合背景下多微网系统的协同规划与运行问题。研究采用多场景分析方法应对可再生能源出力与负荷需求的不确定性,通过上层规划设备容量配置与下层优化多段运行策略的联动,提升系统在复杂环境下的经济性、鲁棒性与能源利用效率。所提供的Matlab代码集成了建模、求解(如YALMIP+CPLEX)与结果可视化全流程,涵盖场景生成与削减、双层优化结构设计及多能流协同调度等关键技术环节,为综合能源系统优化提供了完整的算法实现与技术参考。; 适合人群:具备电力系统、综合能源系统或优化建模背景,熟悉Matlab编程与数学规划方法,正在从事相关领域科研或工程设计工作的研究生、高校研究人员及能源行业技术人员。; 使用场景及目标:①开展冷热电联供(CCHP)多微网系统的容量规划与运行优化研究;②支撑含分布式能源、储能及多能转换设备的综合能源系统多目标、多场景优化建模;③学习与复现双层优化、分布鲁棒优化及场景分析等先进优化方法在能源系统中的实际应用。; 阅读建议:建议结合配套文献与代码同步研读,重点理解双层模型的构建逻辑、变量耦合关系与求解技巧,关注场景生成方法与YALMIP调用细节,通过调整参数、修改目标函数等方式进行仿真实验,以深化对系统优化机理的掌握。
内容概要:本文系统研究了单相逆变器闭环控制下的PWM调制模型,基于Simulink平台构建完整的逆变电路仿真系统,涵盖主电路拓扑、闭环控制器设计、脉宽调制信号生成及输出滤波等关键环节。通过引入比例积分(PI)反馈控制策略,实现对输出电压幅值与波形的精确调节,有效抑制负载扰动带来的影响,提升系统的动态响应能力与稳态精度。仿真过程详细展示了系统建模、参数整定及性能验证的全流程,重点分析了闭环控制在改善输出正弦波质量、降低谐波畸变率方面的优势,为电力电子逆变装置的研发与优化提供了可靠的理论支撑与实践参考。; 适合人群:具备电力电子技术、自动控制原理基础知识及相关仿真经验的高校研究生、科研人员,以及从事新能源发电、不间断电源(UPS)、微电网、电动汽车等领域的工程技术人员。; 使用场景及目标:①掌握单相逆变器闭环控制系统的设计与建模方法;②深入理解PWM技术与反馈控制在逆变系统中的协同工作机制;③通过Simulink仿真平台完成系统搭建与参数调试,服务于课程设计、毕业课题、科研项目或工业产品开发中的逆变器控制算法验证。; 阅读建议:建议结合经典控制理论与电力电子变换技术同步学习,动手复现仿真模型并尝试调整PI控制器参数、载波频率等关键变量,观察其对系统稳定性与输出性能的影响,从而深化对控制机理的理解,并为进一步研究并网逆变、多电平逆变等复杂系统打下坚实基础。
代码转载自:https://pan.quark.cn/s/36f2a379e44e 所讨论的核心内容涉及运用Keras所训练的`.h5`模型对实例进行检测,此任务在深度学习领域内十分普遍。`.h5`作为Keras库保存模型构造与权重的文件类型,使得训练后的模型能够被储存,并在必要被载入以执行预测操作。在开始前,务必确认已配置好Python 3.6的环境,并安装了opencv及Keras相关库。本案例中选用的数据集是MNIST,它是一个常用于手写数字识别的标准数据集。MNIST中的图像均为28x28像素的灰度图,因此在测试个人图像,也需将其调整为相同的图像规格。若手写数字的背景并非黑色,比如呈现白底黑字的情况,可能会对模型的识别能力产生影响,因为模型在训练阶段所适应的是黑底白字的图像。因此,在测试阶段,必须保证图像被转换为黑底白字的格式。测试代码的主要步骤包括:首先,运用`load_model`函数载入`.h5`模型文件,例如使用`model = load_model(fm_cnn_BN.h5)`进行操作。其次,通过`cv2.imread`函数读取图像,再借助`cv2.cvtColor`函数将图像从RGB色彩空间转换为灰度色彩空间。同,要确保图像的尺寸与训练模型的输入尺寸相匹配,一般设定为28x28像素。接着,利用`reshape`方法将图像数据调整至模型所要求的维度。对于MNIST数据集而言,这通常意味着将图像转化为一个一维数组,其形状为`(1, 1, 28, 28)`,其中1代表批次大小,其余部分则分别表示图像的通道数、宽度高度。然后,对数据进行标准化处理,将像素值缩放到0到1的范围内,这通常通过除以255来实现。最后,运用`predict_cl...
内容概要:本文系统阐述了基于数据驱动的模型预测控制(MPC)方法在电力系统机组组合优化中的应用,并以IEEE24节点系统为案例进行了Matlab代码实现。该方法融合实际运行数据,充分发挥MPC滚动优化与反馈校正的优势,对发电机组的启停计划与出力进行多段动态优化,旨在实现电力系统运行的经济性、安全性与可靠性的协同提升。研究内容涵盖优化模型的数学构建、系统约束(如功率平衡、机组爬坡率、最小启停间等)的处理、多目标函数(如燃料成本、启停成本)的设计,以及在MPC框架下的高效求解流程,充分体现了数据驱动方法与先进控制理论在复杂电力系统调度决策中的深度集成与优越性。; 适合人群:具备电力系统分析、优化理论基础及一定Matlab编程能力的研究生、高校科研人员以及从事电力系统调度、能源管理等领域的工程技术人员。; 使用场景及目标:①应用于电力系统日前或实调度中的机组组合问题,为调度员提供科学决策支持;②研究在风电、光伏等新能源出力具有强不确定性的背景下,数据驱动的MPC策略如何提升调度方案的适应性与鲁棒性;③为电力系统优化算法的研究、开发与仿真验证提供一个结构清晰、可复现的技术范例代码参考。; 阅读建议:建议读者结合所提供的完整Matlab代码与IEEE24节点标准系统的详细参数,分模块调试与运行程序,深入理解从数据预处理、模型构建到MPC滚动求解的全过程。在掌握核心逻辑后,可进一步尝试引入更复杂的实际约束条件,或将其拓展应用至其他节点系统或不同的不确定性建模场景中,以深化对方法的理解与创新能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值