=w.SCRIPT.id?r.text():w.DISPLAY:"text"===t&&r.size===w.DISPLAY.size?r=w.TEXT:"script"===t?r=w.SCRIPT:"scriptscript"===t&&(r=w.SCRIPTSCRIPT),r},Or=function(t,e){var r,a=Nr(t.size,e.style),n=a.fracNum(),i=a.fracDen();r=e.havingStyle(n);var o=ue(t.numer,r,e);if(t.continued){var s=8.5/e.fontMetrics().ptPerEm,h=3.5/e.fontMetrics().ptPerEm;o.height=o.height0?3*c:7*c,d=e.fontMetrics().denom1):(m>0?(u=e.fontMetrics().num2,p=c):(u=e.fontMetrics().num3,p=3*c),d=e.fontMetrics().denom2),l){var y=e.fontMetrics().axisHeight;u-o.depth-(y+.5*m)0&&(e="."===(e=t)?null:e),e};Qt({type:"genfrac",names:["\\genfrac"],props:{numArgs:6,greediness:6,argTypes:["math","math","size","text","math","math"]},handler:function(t,e){var r=t.parser,a=e[4],n=e[5],i=Vt(e[0],"atom");i&&(i=Ut(e[0],"open"));var o=i?Er(i.text):null,s=Vt(e[1],"atom");s&&(s=Ut(e[1],"close"));var h,l=s?Er(s.text):null,m=Ft(e[2],"size"),c=null;h=!!m.isBlank||(c=m.value).number>0;var u="auto",p=Vt(e[3],"ordgroup");if(p){if(p.body.length>0){var d=Ft(p.body[0],"textord");u=Rr[Number(d.text)]}}else p=Ft(e[3],"textord"),u=Rr[Number(p.text)];return{type:"genfrac",mode:r.mode,numer:a,denom:n,continued:!1,hasBarLine:h,barSize:c,leftDelim:o,rightDelim:l,size:u}},htmlBuilder:Or,mathmlBuilder:Ir}),Qt({type:"infix",names:["\\above"],props:{numArgs:1,argTypes:["size"],infix:!0},handler:function(t,e){var r=t.parser,a=(t.funcName,t.token);return{type:"infix",mode:r.mode,replaceWith:"\\\\abovefrac",size:Ft(e[0],"size").value,token:a}}}),Qt({type:"genfrac",names:["\\\\abovefrac"],props:{numArgs:3,argTypes:["math","size","math"]},handler:function(t,e){var r=t.parser,a=(t.funcName,e[0]),n=function(t){if(!t)throw new Error("Expected non-null, but got "+String(t));return t}(Ft(e[1],"infix").size),i=e[2],o=n.number>0;return{type:"genfrac",mode:r.mode,numer:a,denom:i,continued:!1,hasBarLine:o,barSize:n,leftDelim:null,rightDelim:null,size:"auto"}},htmlBuilder:Or,mathmlBuilder:Ir});var Lr=function(t,e){var r,a,n=e.style,i=Vt(t,"supsub");i?(r=i.sup?ue(i.sup,e.havingStyle(n.sup()),e):ue(i.sub,e.havingStyle(n.sub()),e),a=Ft(i.base,"horizBrace")):a=Ft(t,"horizBrace");var o,s=ue(a.base,e.havingBaseStyle(w.DISPLAY)),h=Ie(a,e);if(a.isOver?(o=Dt.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:s},{type:"kern",size:.1},{type:"elem",elem:h}]},e)).children[0].children[0].children[1].classes.push("svg-align"):(o=Dt.makeVList({positionType:"bottom",positionData:s.depth+.1+h.height,children:[{type:"elem",elem:h},{type:"kern",size:.1},{type:"elem",elem:s}]},e)).children[0].children[0].children[0].classes.push("svg-align"),r){var l=Dt.makeSpan(["mord",a.isOver?"mover":"munder"],[o],e);o=a.isOver?Dt.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:l},{type:"kern",size:.2},{type:"elem",elem:r}]},e):Dt.makeVList({positionType:"bottom",positionData:l.depth+.2+r.height+r.depth,children:[{type:"elem",elem:r},{type:"kern",size:.2},{type:"elem",elem:l}]},e)}return Dt.makeSpan(["mord",a.isOver?"mover":"munder"],[o],e)};Qt({type:"horizBrace",names:["\\overbrace","\\underbrace"],props:{numArgs:1},handler:function(t,e){var r=t.parser,a=t.funcName;return{type:"horizBrace",mode:r.mode,label:a,isOver:/^\\over/.test(a),base:e[0]}},htmlBuilder:Lr,mathmlBuilder:function(t,e){var r=Oe(t.label);return new ve.MathNode(t.isOver?"mover":"munder",[Me(t.base,e),r])}}),Qt({type:"href",names:["\\href"],props:{numArgs:2,argTypes:["url","original"],allowedInText:!0},handler:function(t,e){var r=t.parser,a=e[1],n=Ft(e[0],"url").url;return r.settings.isTrusted({command:"\\href",url:n})?{type:"href",mode:r.mode,href:n,body:ee(a)}:r.formatUnsupportedCmd("\\href")},htmlBuilder:function(t,e){var r=se(t.body,e,!1);return Dt.makeAnchor(t.href,[],r,e)},mathmlBuilder:function(t,e){var r=Se(t.body,e);return r instanceof ge||(r=new ge("mrow",[r])),r.setAttribute("href",t.href),r}}),Qt({type:"href",names:["\\url"],props:{numArgs:1,argTypes:["url"],allowedInText:!0},handler:function(t,e){var r=t.parser,a=Ft(e[0],"url").url;if(!r.settings.isTrusted({command:"\\url",url:a}))return r.formatUnsupportedCmd("\\url");for(var n=[],i=0;i0&&(a=Tt(t.totalheight,e)-r,a=Number(a.toFixed(2)));var n=0;t.width.number>0&&(n=Tt(t.width,e));var i={height:r+a+"em"};n>0&&(i.width=n+"em"),a>0&&(i.verticalAlign=-a+"em");var o=new I(t.src,t.alt,i);return o.height=r,o.depth=a,o},mathmlBuilder:function(t,e){var r=new ve.MathNode("mglyph",[]);r.setAttribute("alt",t.alt);var a=Tt(t.height,e),n=0;if(t.totalheight.number>0&&(n=(n=Tt(t.totalheight,e)-a).toFixed(2),r.setAttribute("valign","-"+n+"em")),r.setAttribute("height",a+n+"em"),t.width.number>0){var i=Tt(t.width,e);r.setAttribute("width",i+"em")}return r.setAttribute("src",t.src),r}}),Qt({type:"kern",names:["\\kern","\\mkern","\\hskip","\\mskip"],props:{numArgs:1,argTypes:["size"],allowedInText:!0},handler:function(t,e){var r=t.parser,a=t.funcName,n=Ft(e[0],"size");if(r.settings.strict){var i="m"===a[1],o="mu"===n.value.unit;i?(o||r.settings.reportNonstrict("mathVsTextUnits","LaTeX's "+a+" supports only mu units, not "+n.value.unit+" units"),"math"!==r.mode&&r.settings.reportNonstrict("mathVsTextUnits","LaTeX's "+a+" works only in math mode")):o&&r.settings.reportNonstrict("mathVsTextUnits","LaTeX's "+a+" doesn't support mu units")}return{type:"kern",mode:r.mode,dimension:n.value}},htmlBuilder:function(t,e){return Dt.makeGlue(t.dimension,e)},mathmlBuilder:function(t,e){var r=Tt(t.dimension,e);return new ve.SpaceNode(r)}}),Qt({type:"lap",names:["\\mathllap","\\mathrlap","\\mathclap"],props:{numArgs:1,allowedInText:!0},handler:function(t,e){var r=t.parser,a=t.funcName,n=e[0];return{type:"lap",mode:r.mode,alignment:a.slice(5),body:n}},htmlBuilder:function(t,e){var r;"clap"===t.alignment?(r=Dt.makeSpan([],[ue(t.body,e)]),r=Dt.makeSpan(["inner"],[r],e)):r=Dt.makeSpan(["inner"],[ue(t.body,e)]);var a=Dt.makeSpan(["fix"],[]),n=Dt.makeSpan([t.alignment],[r,a],e),i=Dt.makeSpan(["strut"]);return i.style.height=n.height+n.depth+"em",i.style.verticalAlign=-n.depth+"em",n.children.unshift(i),n=Dt.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:n}]},e),Dt.makeSpan(["mord"],[n],e)},mathmlBuilder:function(t,e){var r=new ve.MathNode("mpadded",[Me(t.body,e)]);if("rlap"!==t.alignment){var a="llap"===t.alignment?"-1":"-0.5";r.setAttribute("lspace",a+"width")}return r.setAttribute("width","0px"),r}}),Qt({type:"styling",names:["\\(","$"],props:{numArgs:0,allowedInText:!0,allowedInMath:!1},handler:function(t,e){var r=t.funcName,a=t.parser,n=a.mode;a.switchMode("math");var i="\\("===r?"\\)":"$",o=a.parseExpression(!1,i);return a.expect(i),a.switchMode(n),{type:"styling",mode:a.mode,style:"text",body:o}}}),Qt({type:"text",names:["\\)","\\]"],props:{numArgs:0,allowedInText:!0,allowedInMath:!1},handler:function(t,e){throw new o("Mismatched "+t.funcName)}});var Hr=function(t,e){switch(e.style.size){case w.DISPLAY.size:return t.display;case w.TEXT.size:return t.text;case w.SCRIPT.size:return t.script;case w.SCRIPTSCRIPT.size:return t.scriptscript;default:return t.text}};Qt({type:"mathchoice",names:["\\mathchoice"],props:{numArgs:4},handler:function(t,e){return{type:"mathchoice",mode:t.parser.mode,display:ee(e[0]),text:ee(e[1]),script:ee(e[2]),scriptscript:ee(e[3])}},htmlBuilder:function(t,e){var r=Hr(t,e),a=se(r,e,!1);return Dt.makeFragment(a)},mathmlBuilder:function(t,e){var r=Hr(t,e);return Se(r,e)}});var Dr=function(t,e,r,a,n,i,o){var s,h,l;if(t=Dt.makeSpan([],[t]),e){var m=ue(e,a.havingStyle(n.sup()),a);h={elem:m,kern:Math.max(a.fontMetrics().bigOpSpacing1,a.fontMetrics().bigOpSpacing3-m.depth)}}if(r){var c=ue(r,a.havingStyle(n.sub()),a);s={elem:c,kern:Math.max(a.fontMetrics().bigOpSpacing2,a.fontMetrics().bigOpSpacing4-c.height)}}if(h&&s){var u=a.fontMetrics().bigOpSpacing5+s.elem.height+s.elem.depth+s.kern+t.depth+o;l=Dt.makeVList({positionType:"bottom",positionData:u,children:[{type:"kern",size:a.fontMetrics().bigOpSpacing5},{type:"elem",elem:s.elem,marginLeft:-i+"em"},{type:"kern",size:s.kern},{type:"elem",elem:t},{type:"kern",size:h.kern},{type:"elem",elem:h.elem,marginLeft:i+"em"},{type:"kern",size:a.fontMetrics().bigOpSpacing5}]},a)}else if(s){var p=t.height-o;l=Dt.makeVList({positionType:"top",positionData:p,children:[{type:"kern",size:a.fontMetrics().bigOpSpacing5},{type:"elem",elem:s.elem,marginLeft:-i+"em"},{type:"kern",size:s.kern},{type:"elem",elem:t}]},a)}else{if(!h)return t;var d=t.depth+o;l=Dt.makeVList({positionType:"bottom",positionData:d,children:[{type:"elem",elem:t},{type:"kern",size:h.kern},{type:"elem",elem:h.elem,marginLeft:i+"em"},{type:"kern",size:a.fontMetrics().bigOpSpacing5}]},a)}return Dt.makeSpan(["mop","op-limits"],[l],a)},Fr=["\\smallint"],Vr=function(t,e){var r,a,n,i=!1,o=Vt(t,"supsub");o?(r=o.sup,a=o.sub,n=Ft(o.base,"op"),i=!0):n=Ft(t,"op");var s,h=e.style,l=!1;if(h.size===w.DISPLAY.size&&n.symbol&&!c.contains(Fr,n.name)&&(l=!0),n.symbol){var m=l?"Size2-Regular":"Size1-Regular",u="";if("\\oiint"!==n.name&&"\\oiiint"!==n.name||(u=n.name.substr(1),n.name="oiint"===u?"\\iint":"\\iiint"),s=Dt.makeSymbol(n.name,m,"math",e,["mop","op-symbol",l?"large-op":"small-op"]),u.length>0){var p=s.italic,d=Dt.staticSvg(u+"Size"+(l?"2":"1"),e);s=Dt.makeVList({positionType:"individualShift",children:[{type:"elem",elem:s,shift:0},{type:"elem",elem:d,shift:l?.08:0}]},e),n.name="\\"+u,s.classes.unshift("mop"),s.italic=p}}else if(n.body){var f=se(n.body,e,!0);1===f.length&&f[0]instanceof E?(s=f[0]).classes[0]="mop":s=Dt.makeSpan(["mop"],Dt.tryCombineChars(f),e)}else{for(var g=[],x=1;x0){for(var h=n.body.map((function(t){var e=t.text;return"string"==typeof e?{type:"textord",mode:t.mode,text:e}:t})),l=se(h,e.withFont("mathrm"),!0),m=0;m=0?s.setAttribute("height","+"+n+"em"):(s.setAttribute("height",n+"em"),s.setAttribute("depth","+"+-n+"em")),s.setAttribute("voffset",n+"em"),s}});var Xr=["\\tiny","\\sixptsize","\\scriptsize","\\footnotesize","\\small","\\normalsize","\\large","\\Large","\\LARGE","\\huge","\\Huge"];Qt({type:"sizing",names:Xr,props:{numArgs:0,allowedInText:!0},handler:function(t,e){var r=t.breakOnTokenText,a=t.funcName,n=t.parser,i=n.parseExpression(!1,r);return{type:"sizing",mode:n.mode,size:Xr.indexOf(a)+1,body:i}},htmlBuilder:function(t,e){var r=e.havingSize(t.size);return Wr(t.body,r,e)},mathmlBuilder:function(t,e){var r=e.havingSize(t.size),a=ke(t.body,r),n=new ve.MathNode("mstyle",a);return n.setAttribute("mathsize",r.sizeMultiplier+"em"),n}}),Qt({type:"smash",names:["\\smash"],props:{numArgs:1,numOptionalArgs:1,allowedInText:!0},handler:function(t,e,r){var a=t.parser,n=!1,i=!1,o=r[0]&&Ft(r[0],"ordgroup");if(o)for(var s="",h=0;hr.height+r.depth+i&&(i=(i+c-r.height-r.depth)/2);var u=h.height-r.height-i-l;r.style.paddingLeft=m+"em";var p=Dt.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:r,wrapperClasses:["svg-align"]},{type:"kern",size:-(r.height+u)},{type:"elem",elem:h},{type:"kern",size:l}]},e);if(t.index){var d=e.havingStyle(w.SCRIPTSCRIPT),f=ue(t.index,d,e),g=.6*(p.height-p.depth),x=Dt.makeVList({positionType:"shift",positionData:-g,children:[{type:"elem",elem:f}]},e),v=Dt.makeSpan(["root"],[x]);return Dt.makeSpan(["mord","sqrt"],[v,p],e)}return Dt.makeSpan(["mord","sqrt"],[p],e)},mathmlBuilder:function(t,e){var r=t.body,a=t.index;return a?new ve.MathNode("mroot",[Me(r,e),Me(a,e)]):new ve.MathNode("msqrt",[Me(r,e)])}});var $r={display:w.DISPLAY,text:w.TEXT,script:w.SCRIPT,scriptscript:w.SCRIPTSCRIPT};Qt({type:"styling",names:["\\displaystyle","\\textstyle","\\scriptstyle","\\scriptscriptstyle"],props:{numArgs:0,allowedInText:!0},handler:function(t,e){var r=t.breakOnTokenText,a=t.funcName,n=t.parser,i=n.parseExpression(!0,r),o=a.slice(1,a.length-5);return{type:"styling",mode:n.mode,style:o,body:i}},htmlBuilder:function(t,e){var r=$r[t.style],a=e.havingStyle(r).withFont("");return Wr(t.body,a,e)},mathmlBuilder:function(t,e){var r=$r[t.style],a=e.havingStyle(r),n=ke(t.body,a),i=new ve.MathNode("mstyle",n),o={display:["0","true"],text:["0","false"],script:["1","false"],scriptscript:["2","false"]}[t.style];return i.setAttribute("scriptlevel",o[0]),i.setAttribute("displaystyle",o[1]),i}}),te({type:"supsub",htmlBuilder:function(t,e){var r=function(t,e){var r=t.base;return r?"op"===r.type?r.limits&&(e.style.size===w.DISPLAY.size||r.alwaysHandleSupSub)?Vr:null:"operatorname"===r.type?r.alwaysHandleSupSub&&(e.style.size===w.DISPLAY.size||r.limits)?_r:null:"accent"===r.type?c.isCharacterBox(r.base)?Re:null:"horizBrace"===r.type&&!t.sub===r.isOver?Lr:null:null}(t,e);if(r)return r(t,e);var a,n,i,o=t.base,s=t.sup,h=t.sub,l=ue(o,e),m=e.fontMetrics(),u=0,p=0,d=o&&c.isCharacterBox(o);if(s){var f=e.havingStyle(e.style.sup());a=ue(s,f,e),d||(u=l.height-f.fontMetrics().supDrop*f.sizeMultiplier/e.sizeMultiplier)}if(h){var g=e.havingStyle(e.style.sub());n=ue(h,g,e),d||(p=l.depth+g.fontMetrics().subDrop*g.sizeMultiplier/e.sizeMultiplier)}i=e.style===w.DISPLAY?m.sup1:e.style.cramped?m.sup3:m.sup2;var x,v=e.sizeMultiplier,b=.5/m.ptPerEm/v+"em",y=null;if(n){var k=t.base&&"op"===t.base.type&&t.base.name&&("\\oiint"===t.base.name||"\\oiiint"===t.base.name);(l instanceof E||k)&&(y=-l.italic+"em")}if(a&&n){u=Math.max(u,i,a.depth+.25*m.xHeight),p=Math.max(p,m.sub2);var S=4*m.defaultRuleThickness;if(u-a.depth-(n.height-p)0&&(u+=M,p-=M)}var z=[{type:"elem",elem:n,shift:p,marginRight:b,marginLeft:y},{type:"elem",elem:a,shift:-u,marginRight:b}];x=Dt.makeVList({positionType:"individualShift",children:z},e)}else if(n){p=Math.max(p,m.sub1,n.height-.8*m.xHeight);var A=[{type:"elem",elem:n,marginLeft:y,marginRight:b}];x=Dt.makeVList({positionType:"shift",positionData:p,children:A},e)}else{if(!a)throw new Error("supsub must have either sup or sub.");u=Math.max(u,i,a.depth+.25*m.xHeight),x=Dt.makeVList({positionType:"shift",positionData:-u,children:[{type:"elem",elem:a,marginRight:b}]},e)}var T=me(l,"right")||"mord";return Dt.makeSpan([T],[l,Dt.makeSpan(["msupsub"],[x])],e)},mathmlBuilder:function(t,e){var r,a=!1,n=Vt(t.base,"horizBrace");n&&!!t.sup===n.isOver&&(a=!0,r=n.isOver),!t.base||"op"!==t.base.type&&"operatorname"!==t.base.type||(t.base.parentIsSupSub=!0);var i,o=[Me(t.base,e)];if(t.sub&&o.push(Me(t.sub,e)),t.sup&&o.push(Me(t.sup,e)),a)i=r?"mover":"munder";else if(t.sub)if(t.sup){var s=t.base;i=s&&"op"===s.type&&s.limits&&e.style===w.DISPLAY||s&&"operatorname"===s.type&&s.alwaysHandleSupSub&&(e.style===w.DISPLAY||s.limits)?"munderover":"msubsup"}else{var h=t.base;i=h&&"op"===h.type&&h.limits&&(e.style===w.DISPLAY||h.alwaysHandleSupSub)||h&&"operatorname"===h.type&&h.alwaysHandleSupSub&&(h.limits||e.style===w.DISPLAY)?"munder":"msub"}else{var l=t.base;i=l&&"op"===l.type&&l.limits&&(e.style===w.DISPLAY||l.alwaysHandleSupSub)||l&&"operatorname"===l.type&&l.alwaysHandleSupSub&&(l.limits||e.style===w.DISPLAY)?"mover":"msup"}return new ve.MathNode(i,o)}}),te({type:"atom",htmlBuilder:function(t,e){return Dt.mathsym(t.text,t.mode,e,["m"+t.family])},mathmlBuilder:function(t,e){var r=new ve.MathNode("mo",[be(t.text,t.mode)]);if("bin"===t.family){var a=we(t,e);"bold-italic"===a&&r.setAttribute("mathvariant",a)}else"punct"===t.family?r.setAttribute("separator","true"):"open"!==t.family&&"close"!==t.family||r.setAttribute("stretchy","false");return r}});var jr={mi:"italic",mn:"normal",mtext:"normal"};te({type:"mathord",htmlBuilder:function(t,e){return Dt.makeOrd(t,e,"mathord")},mathmlBuilder:function(t,e){var r=new ve.MathNode("mi",[be(t.text,t.mode,e)]),a=we(t,e)||"italic";return a!==jr[r.type]&&r.setAttribute("mathvariant",a),r}}),te({type:"textord",htmlBuilder:function(t,e){return Dt.makeOrd(t,e,"textord")},mathmlBuilder:function(t,e){var r,a=be(t.text,t.mode,e),n=we(t,e)||"normal";return r="text"===t.mode?new ve.MathNode("mtext",[a]):/[0-9]/.test(t.text)?new ve.MathNode("mn",[a]):"\\prime"===t.text?new ve.MathNode("mo",[a]):new ve.MathNode("mi",[a]),n!==jr[r.type]&&r.setAttribute("mathvariant",n),r}});var Zr={"\\nobreak":"nobreak","\\allowbreak":"allowbreak"},Kr={" ":{},"\\ ":{},"~":{className:"nobreak"},"\\space":{},"\\nobreakspace":{className:"nobreak"}};te({type:"spacing",htmlBuilder:function(t,e){if(Kr.hasOwnProperty(t.text)){var r=Kr[t.text].className||"";if("text"===t.mode){var a=Dt.makeOrd(t,e,"textord");return a.classes.push(r),a}return Dt.makeSpan(["mspace",r],[Dt.mathsym(t.text,t.mode,e)],e)}if(Zr.hasOwnProperty(t.text))return Dt.makeSpan(["mspace",Zr[t.text]],[],e);throw new o('Unknown type of space "'+t.text+'"')},mathmlBuilder:function(t,e){if(!Kr.hasOwnProperty(t.text)){if(Zr.hasOwnProperty(t.text))return new ve.MathNode("mspace");throw new o('Unknown type of space "'+t.text+'"')}return new ve.MathNode("mtext",[new ve.TextNode(" ")])}});var Jr=function(){var t=new ve.MathNode("mtd",[]);return t.setAttribute("width","50%"),t};te({type:"tag",mathmlBuilder:function(t,e){var r=new ve.MathNode("mtable",[new ve.MathNode("mtr",[Jr(),new ve.MathNode("mtd",[Se(t.body,e)]),Jr(),new ve.MathNode("mtd",[Se(t.tag,e)])])]);return r.setAttribute("width","100%"),r}});var Qr={"\\text":void 0,"\\textrm":"textrm","\\textsf":"textsf","\\texttt":"texttt","\\textnormal":"textrm"},ta={"\\textbf":"textbf","\\textmd":"textmd"},ea={"\\textit":"textit","\\textup":"textup"},ra=function(t,e){var r=t.font;return r?Qr[r]?e.withTextFontFamily(Qr[r]):ta[r]?e.withTextFontWeight(ta[r]):e.withTextFontShape(ea[r]):e};Qt({type:"text",names:["\\text","\\textrm","\\textsf","\\texttt","\\textnormal","\\textbf","\\textmd","\\textit","\\textup"],props:{numArgs:1,argTypes:["text"],greediness:2,allowedInText:!0},handler:function(t,e){var r=t.parser,a=t.funcName,n=e[0];return{type:"text",mode:r.mode,body:ee(n),font:a}},htmlBuilder:function(t,e){var r=ra(t,e),a=se(t.body,r,!0);return Dt.makeSpan(["mord","text"],Dt.tryCombineChars(a),r)},mathmlBuilder:function(t,e){var r=ra(t,e);return Se(t.body,r)}}),Qt({type:"underline",names:["\\underline"],props:{numArgs:1,allowedInText:!0},handler:function(t,e){return{type:"underline",mode:t.parser.mode,body:e[0]}},htmlBuilder:function(t,e){var r=ue(t.body,e),a=Dt.makeLineSpan("underline-line",e),n=e.fontMetrics().defaultRuleThickness,i=Dt.makeVList({positionType:"top",positionData:r.height,children:[{type:"kern",size:n},{type:"elem",elem:a},{type:"kern",size:3*n},{type:"elem",elem:r}]},e);return Dt.makeSpan(["mord","underline"],[i],e)},mathmlBuilder:function(t,e){var r=new ve.MathNode("mo",[new ve.TextNode("‾")]);r.setAttribute("stretchy","true");var a=new ve.MathNode("munder",[Me(t.body,e),r]);return a.setAttribute("accentunder","true"),a}}),Qt({type:"verb",names:["\\verb"],props:{numArgs:0,allowedInText:!0},handler:function(t,e,r){throw new o("\\verb ended by end of line instead of matching delimiter")},htmlBuilder:function(t,e){for(var r=aa(t),a=[],n=e.havingStyle(e.style.text()),i=0;i0&&(this.undefStack[this.undefStack.length-1][t]=e)}else{var n=this.undefStack[this.undefStack.length-1];n&&!n.hasOwnProperty(t)&&(n[t]=this.current[t])}this.current[t]=e},t}(),la={},ma=la;function ca(t,e){la[t]=e}ca("\\@firstoftwo",(function(t){return{tokens:t.consumeArgs(2)[0],numArgs:0}})),ca("\\@secondoftwo",(function(t){return{tokens:t.consumeArgs(2)[1],numArgs:0}})),ca("\\@ifnextchar",(function(t){var e=t.consumeArgs(3),r=t.future();return 1===e[0].length&&e[0][0].text===r.text?{tokens:e[1],numArgs:0}:{tokens:e[2],numArgs:0}})),ca("\\@ifstar","\\@ifnextchar *{\\@firstoftwo{#1}}"),ca("\\TextOrMath",(function(t){var e=t.consumeArgs(2);return"text"===t.mode?{tokens:e[0],numArgs:0}:{tokens:e[1],numArgs:0}}));var ua={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,a:10,A:10,b:11,B:11,c:12,C:12,d:13,D:13,e:14,E:14,f:15,F:15};ca("\\char",(function(t){var e,r=t.popToken(),a="";if("'"===r.text)e=8,r=t.popToken();else if('"'===r.text)e=16,r=t.popToken();else if("`"===r.text)if("\\"===(r=t.popToken()).text[0])a=r.text.charCodeAt(1);else{if("EOF"===r.text)throw new o("\\char` missing argument");a=r.text.charCodeAt(0)}else e=10;if(e){if(null==(a=ua[r.text])||a>=e)throw new o("Invalid base-"+e+" digit "+r.text);for(var n;null!=(n=ua[t.future().text])&&n":"\\dotsb","-":"\\dotsb","*":"\\dotsb",":":"\\dotsb","\\DOTSB":"\\dotsb","\\coprod":"\\dotsb","\\bigvee":"\\dotsb","\\bigwedge":"\\dotsb","\\biguplus":"\\dotsb","\\bigcap":"\\dotsb","\\bigcup":"\\dotsb","\\prod":"\\dotsb","\\sum":"\\dotsb","\\bigotimes":"\\dotsb","\\bigoplus":"\\dotsb","\\bigodot":"\\dotsb","\\bigsqcup":"\\dotsb","\\And":"\\dotsb","\\longrightarrow":"\\dotsb","\\Longrightarrow":"\\dotsb","\\longleftarrow":"\\dotsb","\\Longleftarrow":"\\dotsb","\\longleftrightarrow":"\\dotsb","\\Longleftrightarrow":"\\dotsb","\\mapsto":"\\dotsb","\\longmapsto":"\\dotsb","\\hookrightarrow":"\\dotsb","\\doteq":"\\dotsb","\\mathbin":"\\dotsb","\\mathrel":"\\dotsb","\\relbar":"\\dotsb","\\Relbar":"\\dotsb","\\xrightarrow":"\\dotsb","\\xleftarrow":"\\dotsb","\\DOTSI":"\\dotsi","\\int":"\\dotsi","\\oint":"\\dotsi","\\iint":"\\dotsi","\\iiint":"\\dotsi","\\iiiint":"\\dotsi","\\idotsint":"\\dotsi","\\DOTSX":"\\dotsx"};ca("\\dots",(function(t){var e="\\dotso",r=t.expandAfterFuture().text;return r in fa?e=fa[r]:("\\not"===r.substr(0,4)||r in $.math&&c.contains(["bin","rel"],$.math[r].group))&&(e="\\dotsb"),e}));var ga={")":!0,"]":!0,"\\rbrack":!0,"\\}":!0,"\\rbrace":!0,"\\rangle":!0,"\\rceil":!0,"\\rfloor":!0,"\\rgroup":!0,"\\rmoustache":!0,"\\right":!0,"\\bigr":!0,"\\biggr":!0,"\\Bigr":!0,"\\Biggr":!0,$:!0,";":!0,".":!0,",":!0};ca("\\dotso",(function(t){return t.future().text in ga?"\\ldots\\,":"\\ldots"})),ca("\\dotsc",(function(t){var e=t.future().text;return e in ga&&","!==e?"\\ldots\\,":"\\ldots"})),ca("\\cdots",(function(t){return t.future().text in ga?"\\@cdots\\,":"\\@cdots"})),ca("\\dotsb","\\cdots"),ca("\\dotsm","\\cdots"),ca("\\dotsi","\\!\\cdots"),ca("\\dotsx","\\ldots\\,"),ca("\\DOTSI","\\relax"),ca("\\DOTSB","\\relax"),ca("\\DOTSX","\\relax"),ca("\\tmspace","\\TextOrMath{\\kern#1#3}{\\mskip#1#2}\\relax"),ca("\\,","\\tmspace+{3mu}{.1667em}"),ca("\\thinspace","\\,"),ca("\\>","\\mskip{4mu}"),ca("\\:","\\tmspace+{4mu}{.2222em}"),ca("\\medspace","\\:"),ca("\\;","\\tmspace+{5mu}{.2777em}"),ca("\\thickspace","\\;"),ca("\\!","\\tmspace-{3mu}{.1667em}"),ca("\\negthinspace","\\!"),ca("\\negmedspace","\\tmspace-{4mu}{.2222em}"),ca("\\negthickspace","\\tmspace-{5mu}{.277em}"),ca("\\enspace","\\kern.5em "),ca("\\enskip","\\hskip.5em\\relax"),ca("\\quad","\\hskip1em\\relax"),ca("\\qquad","\\hskip2em\\relax"),ca("\\tag","\\@ifstar\\tag@literal\\tag@paren"),ca("\\tag@paren","\\tag@literal{({#1})}"),ca("\\tag@literal",(function(t){if(t.macros.get("\\df@tag"))throw new o("Multiple \\tag");return"\\gdef\\df@tag{\\text{#1}}"})),ca("\\bmod","\\mathchoice{\\mskip1mu}{\\mskip1mu}{\\mskip5mu}{\\mskip5mu}\\mathbin{\\rm mod}\\mathchoice{\\mskip1mu}{\\mskip1mu}{\\mskip5mu}{\\mskip5mu}"),ca("\\pod","\\allowbreak\\mathchoice{\\mkern18mu}{\\mkern8mu}{\\mkern8mu}{\\mkern8mu}(#1)"),ca("\\pmod","\\pod{{\\rm mod}\\mkern6mu#1}"),ca("\\mod","\\allowbreak\\mathchoice{\\mkern18mu}{\\mkern12mu}{\\mkern12mu}{\\mkern12mu}{\\rm mod}\\,\\,#1"),ca("\\pmb","\\html@mathml{\\@binrel{#1}{\\mathrlap{#1}\\kern0.5px#1}}{\\mathbf{#1}}"),ca("\\\\","\\newline"),ca("\\TeX","\\textrm{\\html@mathml{T\\kern-.1667em\\raisebox{-.5ex}{E}\\kern-.125emX}{TeX}}");var xa=F["Main-Regular"]["T".charCodeAt(0)][1]-.7*F["Main-Regular"]["A".charCodeAt(0)][1]+"em";ca("\\LaTeX","\\textrm{\\html@mathml{L\\kern-.36em\\raisebox{"+xa+"}{\\scriptstyle A}\\kern-.15em\\TeX}{LaTeX}}"),ca("\\KaTeX","\\textrm{\\html@mathml{K\\kern-.17em\\raisebox{"+xa+"}{\\scriptstyle A}\\kern-.15em\\TeX}{KaTeX}}"),ca("\\hspace","\\@ifstar\\@hspacer\\@hspace"),ca("\\@hspace","\\hskip #1\\relax"),ca("\\@hspacer","\\rule{0pt}{0pt}\\hskip #1\\relax"),ca("\\ordinarycolon",":"),ca("\\vcentcolon","\\mathrel{\\mathop\\ordinarycolon}"),ca("\\dblcolon",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-.9mu}\\vcentcolon}}{\\mathop{\\char"2237}}'),ca("\\coloneqq",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}=}}{\\mathop{\\char"2254}}'),ca("\\Coloneqq",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}=}}{\\mathop{\\char"2237\\char"3d}}'),ca("\\coloneq",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\mathrel{-}}}{\\mathop{\\char"3a\\char"2212}}'),ca("\\Coloneq",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\mathrel{-}}}{\\mathop{\\char"2237\\char"2212}}'),ca("\\eqqcolon",'\\html@mathml{\\mathrel{=\\mathrel{\\mkern-1.2mu}\\vcentcolon}}{\\mathop{\\char"2255}}'),ca("\\Eqqcolon",'\\html@mathml{\\mathrel{=\\mathrel{\\mkern-1.2mu}\\dblcolon}}{\\mathop{\\char"3d\\char"2237}}'),ca("\\eqcolon",'\\html@mathml{\\mathrel{\\mathrel{-}\\mathrel{\\mkern-1.2mu}\\vcentcolon}}{\\mathop{\\char"2239}}'),ca("\\Eqcolon",'\\html@mathml{\\mathrel{\\mathrel{-}\\mathrel{\\mkern-1.2mu}\\dblcolon}}{\\mathop{\\char"2212\\char"2237}}'),ca("\\colonapprox",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\approx}}{\\mathop{\\char"3a\\char"2248}}'),ca("\\Colonapprox",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\approx}}{\\mathop{\\char"2237\\char"2248}}'),ca("\\colonsim",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\sim}}{\\mathop{\\char"3a\\char"223c}}'),ca("\\Colonsim",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\sim}}{\\mathop{\\char"2237\\char"223c}}'),ca("∷","\\dblcolon"),ca("∹","\\eqcolon"),ca("≔","\\coloneqq"),ca("≕","\\eqqcolon"),ca("⩴","\\Coloneqq"),ca("\\ratio","\\vcentcolon"),ca("\\coloncolon","\\dblcolon"),ca("\\colonequals","\\coloneqq"),ca("\\coloncolonequals","\\Coloneqq"),ca("\\equalscolon","\\eqqcolon"),ca("\\equalscoloncolon","\\Eqqcolon"),ca("\\colonminus","\\coloneq"),ca("\\coloncolonminus","\\Coloneq"),ca("\\minuscolon","\\eqcolon"),ca("\\minuscoloncolon","\\Eqcolon"),ca("\\coloncolonapprox","\\Colonapprox"),ca("\\coloncolonsim","\\Colonsim"),ca("\\simcolon","\\mathrel{\\sim\\mathrel{\\mkern-1.2mu}\\vcentcolon}"),ca("\\simcoloncolon","\\mathrel{\\sim\\mathrel{\\mkern-1.2mu}\\dblcolon}"),ca("\\approxcolon","\\mathrel{\\approx\\mathrel{\\mkern-1.2mu}\\vcentcolon}"),ca("\\approxcoloncolon","\\mathrel{\\approx\\mathrel{\\mkern-1.2mu}\\dblcolon}"),ca("\\notni","\\html@mathml{\\not\\ni}{\\mathrel{\\char`∌}}"),ca("\\limsup","\\DOTSB\\operatorname*{lim\\,sup}"),ca("\\liminf","\\DOTSB\\operatorname*{lim\\,inf}"),ca("\\gvertneqq","\\html@mathml{\\@gvertneqq}{≩}"),ca("\\lvertneqq","\\html@mathml{\\@lvertneqq}{≨}"),ca("\\ngeqq","\\html@mathml{\\@ngeqq}{≱}"),ca("\\ngeqslant","\\html@mathml{\\@ngeqslant}{≱}"),ca("\\nleqq","\\html@mathml{\\@nleqq}{≰}"),ca("\\nleqslant","\\html@mathml{\\@nleqslant}{≰}"),ca("\\nshortmid","\\html@mathml{\\@nshortmid}{∤}"),ca("\\nshortparallel","\\html@mathml{\\@nshortparallel}{∦}"),ca("\\nsubseteqq","\\html@mathml{\\@nsubseteqq}{⊈}"),ca("\\nsupseteqq","\\html@mathml{\\@nsupseteqq}{⊉}"),ca("\\varsubsetneq","\\html@mathml{\\@varsubsetneq}{⊊}"),ca("\\varsubsetneqq","\\html@mathml{\\@varsubsetneqq}{⫋}"),ca("\\varsupsetneq","\\html@mathml{\\@varsupsetneq}{⊋}"),ca("\\varsupsetneqq","\\html@mathml{\\@varsupsetneqq}{⫌}"),ca("\\llbracket","\\html@mathml{\\mathopen{[\\mkern-3.2mu[}}{\\mathopen{\\char`⟦}}"),ca("\\rrbracket","\\html@mathml{\\mathclose{]\\mkern-3.2mu]}}{\\mathclose{\\char`⟧}}"),ca("⟦","\\llbracket"),ca("⟧","\\rrbracket"),ca("\\lBrace","\\html@mathml{\\mathopen{\\{\\mkern-3.2mu[}}{\\mathopen{\\char`⦃}}"),ca("\\rBrace","\\html@mathml{\\mathclose{]\\mkern-3.2mu\\}}}{\\mathclose{\\char`⦄}}"),ca("⦃","\\lBrace"),ca("⦄","\\rBrace"),ca("\\darr","\\downarrow"),ca("\\dArr","\\Downarrow"),ca("\\Darr","\\Downarrow"),ca("\\lang","\\langle"),ca("\\rang","\\rangle"),ca("\\uarr","\\uparrow"),ca("\\uArr","\\Uparrow"),ca("\\Uarr","\\Uparrow"),ca("\\N","\\mathbb{N}"),ca("\\R","\\mathbb{R}"),ca("\\Z","\\mathbb{Z}"),ca("\\alef","\\aleph"),ca("\\alefsym","\\aleph"),ca("\\Alpha","\\mathrm{A}"),ca("\\Beta","\\mathrm{B}"),ca("\\bull","\\bullet"),ca("\\Chi","\\mathrm{X}"),ca("\\clubs","\\clubsuit"),ca("\\cnums","\\mathbb{C}"),ca("\\Complex","\\mathbb{C}"),ca("\\Dagger","\\ddagger"),ca("\\diamonds","\\diamondsuit"),ca("\\empty","\\emptyset"),ca("\\Epsilon","\\mathrm{E}"),ca("\\Eta","\\mathrm{H}"),ca("\\exist","\\exists"),ca("\\harr","\\leftrightarrow"),ca("\\hArr","\\Leftrightarrow"),ca("\\Harr","\\Leftrightarrow"),ca("\\hearts","\\heartsuit"),ca("\\image","\\Im"),ca("\\infin","\\infty"),ca("\\Iota","\\mathrm{I}"),ca("\\isin","\\in"),ca("\\Kappa","\\mathrm{K}"),ca("\\larr","\\leftarrow"),ca("\\lArr","\\Leftarrow"),ca("\\Larr","\\Leftarrow"),ca("\\lrarr","\\leftrightarrow"),ca("\\lrArr","\\Leftrightarrow"),ca("\\Lrarr","\\Leftrightarrow"),ca("\\Mu","\\mathrm{M}"),ca("\\natnums","\\mathbb{N}"),ca("\\Nu","\\mathrm{N}"),ca("\\Omicron","\\mathrm{O}"),ca("\\plusmn","\\pm"),ca("\\rarr","\\rightarrow"),ca("\\rArr","\\Rightarrow"),ca("\\Rarr","\\Rightarrow"),ca("\\real","\\Re"),ca("\\reals","\\mathbb{R}"),ca("\\Reals","\\mathbb{R}"),ca("\\Rho","\\mathrm{P}"),ca("\\sdot","\\cdot"),ca("\\sect","\\S"),ca("\\spades","\\spadesuit"),ca("\\sub","\\subset"),ca("\\sube","\\subseteq"),ca("\\supe","\\supseteq"),ca("\\Tau","\\mathrm{T}"),ca("\\thetasym","\\vartheta"),ca("\\weierp","\\wp"),ca("\\Zeta","\\mathrm{Z}"),ca("\\argmin","\\DOTSB\\operatorname*{arg\\,min}"),ca("\\argmax","\\DOTSB\\operatorname*{arg\\,max}"),ca("\\plim","\\DOTSB\\mathop{\\operatorname{plim}}\\limits"),ca("\\blue","\\textcolor{##6495ed}{#1}"),ca("\\orange","\\textcolor{##ffa500}{#1}"),ca("\\pink","\\textcolor{##ff00af}{#1}"),ca("\\red","\\textcolor{##df0030}{#1}"),ca("\\green","\\textcolor{##28ae7b}{#1}"),ca("\\gray","\\textcolor{gray}{#1}"),ca("\\purple","\\textcolor{##9d38bd}{#1}"),ca("\\blueA","\\textcolor{##ccfaff}{#1}"),ca("\\blueB","\\textcolor{##80f6ff}{#1}"),ca("\\blueC","\\textcolor{##63d9ea}{#1}"),ca("\\blueD","\\textcolor{##11accd}{#1}"),ca("\\blueE","\\textcolor{##0c7f99}{#1}"),ca("\\tealA","\\textcolor{##94fff5}{#1}"),ca("\\tealB","\\textcolor{##26edd5}{#1}"),ca("\\tealC","\\textcolor{##01d1c1}{#1}"),ca("\\tealD","\\textcolor{##01a995}{#1}"),ca("\\tealE","\\textcolor{##208170}{#1}"),ca("\\greenA","\\textcolor{##b6ffb0}{#1}"),ca("\\greenB","\\textcolor{##8af281}{#1}"),ca("\\greenC","\\textcolor{##74cf70}{#1}"),ca("\\greenD","\\textcolor{##1fab54}{#1}"),ca("\\greenE","\\textcolor{##0d923f}{#1}"),ca("\\goldA","\\textcolor{##ffd0a9}{#1}"),ca("\\goldB","\\textcolor{##ffbb71}{#1}"),ca("\\goldC","\\textcolor{##ff9c39}{#1}"),ca("\\goldD","\\textcolor{##e07d10}{#1}"),ca("\\goldE","\\textcolor{##a75a05}{#1}"),ca("\\redA","\\textcolor{##fca9a9}{#1}"),ca("\\redB","\\textcolor{##ff8482}{#1}"),ca("\\redC","\\textcolor{##f9685d}{#1}"),ca("\\redD","\\textcolor{##e84d39}{#1}"),ca("\\redE","\\textcolor{##bc2612}{#1}"),ca("\\maroonA","\\textcolor{##ffbde0}{#1}"),ca("\\maroonB","\\textcolor{##ff92c6}{#1}"),ca("\\maroonC","\\textcolor{##ed5fa6}{#1}"),ca("\\maroonD","\\textcolor{##ca337c}{#1}"),ca("\\maroonE","\\textcolor{##9e034e}{#1}"),ca("\\purpleA","\\textcolor{##ddd7ff}{#1}"),ca("\\purpleB","\\textcolor{##c6b9fc}{#1}"),ca("\\purpleC","\\textcolor{##aa87ff}{#1}"),ca("\\purpleD","\\textcolor{##7854ab}{#1}"),ca("\\purpleE","\\textcolor{##543b78}{#1}"),ca("\\mintA","\\textcolor{##f5f9e8}{#1}"),ca("\\mintB","\\textcolor{##edf2df}{#1}"),ca("\\mintC","\\textcolor{##e0e5cc}{#1}"),ca("\\grayA","\\textcolor{##f6f7f7}{#1}"),ca("\\grayB","\\textcolor{##f0f1f2}{#1}"),ca("\\grayC","\\textcolor{##e3e5e6}{#1}"),ca("\\grayD","\\textcolor{##d6d8da}{#1}"),ca("\\grayE","\\textcolor{##babec2}{#1}"),ca("\\grayF","\\textcolor{##888d93}{#1}"),ca("\\grayG","\\textcolor{##626569}{#1}"),ca("\\grayH","\\textcolor{##3b3e40}{#1}"),ca("\\grayI","\\textcolor{##21242c}{#1}"),ca("\\kaBlue","\\textcolor{##314453}{#1}"),ca("\\kaGreen","\\textcolor{##71B307}{#1}");var va={"\\relax":!0,"^":!0,_:!0,"\\limits":!0,"\\nolimits":!0},ba=function(){function t(t,e,r){this.settings=void 0,this.expansionCount=void 0,this.lexer=void 0,this.macros=void 0,this.stack=void 0,this.mode=void 0,this.settings=e,this.expansionCount=0,this.feed(t),this.macros=new ha(ma,e.macros),this.mode=r,this.stack=[]}var e=t.prototype;return e.feed=function(t){this.lexer=new sa(t,this.settings)},e.switchMode=function(t){this.mode=t},e.beginGroup=function(){this.macros.beginGroup()},e.endGroup=function(){this.macros.endGroup()},e.future=function(){return 0===this.stack.length&&this.pushToken(this.lexer.lex()),this.stack[this.stack.length-1]},e.popToken=function(){return this.future(),this.stack.pop()},e.pushToken=function(t){this.stack.push(t)},e.pushTokens=function(t){var e;(e=this.stack).push.apply(e,t)},e.consumeSpaces=function(){for(;" "===this.future().text;)this.stack.pop()},e.consumeArgs=function(t){for(var e=[],r=0;rthis.settings.maxExpand)throw new o("Too many expansions: infinite loop or need to increase maxExpand setting");var a=r.tokens;if(r.numArgs)for(var n=this.consumeArgs(r.numArgs),i=(a=a.slice()).length-1;i>=0;--i){var s=a[i];if("#"===s.text){if(0===i)throw new o("Incomplete placeholder at end of macro body",s);if("#"===(s=a[--i]).text)a.splice(i+1,1);else{if(!/^[1-9]$/.test(s.text))throw new o("Not a valid argument number",s);var h;(h=a).splice.apply(h,[i,2].concat(n[+s.text-1]))}}}return this.pushTokens(a),a},e.expandAfterFuture=function(){return this.expandOnce(),this.future()},e.expandNextToken=function(){for(;;){var t=this.expandOnce();if(t instanceof n){if("\\relax"!==t.text)return this.stack.pop();this.stack.pop()}}throw new Error},e.expandMacro=function(t){if(this.macros.get(t)){var e=[],r=this.stack.length;for(this.pushToken(new n(t));this.stack.length>r;)this.expandOnce()instanceof n&&e.push(this.stack.pop());return e}},e.expandMacroAsText=function(t){var e=this.expandMacro(t);return e?e.map((function(t){return t.text})).join(""):e},e._getExpansion=function(t){var e=this.macros.get(t);if(null==e)return e;var r="function"==typeof e?e(this):e;if("string"==typeof r){var a=0;if(-1!==r.indexOf("#"))for(var n=r.replace(/##/g,"");-1!==n.indexOf("#"+(a+1));)++a;for(var i=new sa(r,this.settings),o=[],s=i.lex();"EOF"!==s.text;)o.push(s),s=i.lex();return o.reverse(),{tokens:o,numArgs:a}}return r},e.isDefined=function(t){return this.macros.has(t)||na.hasOwnProperty(t)||$.math.hasOwnProperty(t)||$.text.hasOwnProperty(t)||va.hasOwnProperty(t)},t}(),ya={"́":{text:"\\'",math:"\\acute"},"̀":{text:"\\`",math:"\\grave"},"̈":{text:'\\"',math:"\\ddot"},"̃":{text:"\\~",math:"\\tilde"},"̄":{text:"\\=",math:"\\bar"},"̆":{text:"\\u",math:"\\breve"},"̌":{text:"\\v",math:"\\check"},"̂":{text:"\\^",math:"\\hat"},"̇":{text:"\\.",math:"\\dot"},"̊":{text:"\\r",math:"\\mathring"},"̋":{text:"\\H"}},wa={"á":"á","à":"à","ä":"ä","ǟ":"ǟ","ã":"ã","ā":"ā","ă":"ă","ắ":"ắ","ằ":"ằ","ẵ":"ẵ","ǎ":"ǎ","â":"â","ấ":"ấ","ầ":"ầ","ẫ":"ẫ","ȧ":"ȧ","ǡ":"ǡ","å":"å","ǻ":"ǻ","ḃ":"ḃ","ć":"ć","č":"č","ĉ":"ĉ","ċ":"ċ","ď":"ď","ḋ":"ḋ","é":"é","è":"è","ë":"ë","ẽ":"ẽ","ē":"ē","ḗ":"ḗ","ḕ":"ḕ","ĕ":"ĕ","ě":"ě","ê":"ê","ế":"ế","ề":"ề","ễ":"ễ","ė":"ė","ḟ":"ḟ","ǵ":"ǵ","ḡ":"ḡ","ğ":"ğ","ǧ":"ǧ","ĝ":"ĝ","ġ":"ġ","ḧ":"ḧ","ȟ":"ȟ","ĥ":"ĥ","ḣ":"ḣ","í":"í","ì":"ì","ï":"ï","ḯ":"ḯ","ĩ":"ĩ","ī":"ī","ĭ":"ĭ","ǐ":"ǐ","î":"î","ǰ":"ǰ","ĵ":"ĵ","ḱ":"ḱ","ǩ":"ǩ","ĺ":"ĺ","ľ":"ľ","ḿ":"ḿ","ṁ":"ṁ","ń":"ń","ǹ":"ǹ","ñ":"ñ","ň":"ň","ṅ":"ṅ","ó":"ó","ò":"ò","ö":"ö","ȫ":"ȫ","õ":"õ","ṍ":"ṍ","ṏ":"ṏ","ȭ":"ȭ","ō":"ō","ṓ":"ṓ","ṑ":"ṑ","ŏ":"ŏ","ǒ":"ǒ","ô":"ô","ố":"ố","ồ":"ồ","ỗ":"ỗ","ȯ":"ȯ","ȱ":"ȱ","ő":"ő","ṕ":"ṕ","ṗ":"ṗ","ŕ":"ŕ","ř":"ř","ṙ":"ṙ","ś":"ś","ṥ":"ṥ","š":"š","ṧ":"ṧ","ŝ":"ŝ","ṡ":"ṡ","ẗ":"ẗ","ť":"ť","ṫ":"ṫ","ú":"ú","ù":"ù","ü":"ü","ǘ":"ǘ","ǜ":"ǜ","ǖ":"ǖ","ǚ":"ǚ","ũ":"ũ","ṹ":"ṹ","ū":"ū","ṻ":"ṻ","ŭ":"ŭ","ǔ":"ǔ","û":"û","ů":"ů","ű":"ű","ṽ":"ṽ","ẃ":"ẃ","ẁ":"ẁ","ẅ":"ẅ","ŵ":"ŵ","ẇ":"ẇ","ẘ":"ẘ","ẍ":"ẍ","ẋ":"ẋ","ý":"ý","ỳ":"ỳ","ÿ":"ÿ","ỹ":"ỹ","ȳ":"ȳ","ŷ":"ŷ","ẏ":"ẏ","ẙ":"ẙ","ź":"ź","ž":"ž","ẑ":"ẑ","ż":"ż","Á":"Á","À":"À","Ä":"Ä","Ǟ":"Ǟ","Ã":"Ã","Ā":"Ā","Ă":"Ă","Ắ":"Ắ","Ằ":"Ằ","Ẵ":"Ẵ","Ǎ":"Ǎ","Â":"Â","Ấ":"Ấ","Ầ":"Ầ","Ẫ":"Ẫ","Ȧ":"Ȧ","Ǡ":"Ǡ","Å":"Å","Ǻ":"Ǻ","Ḃ":"Ḃ","Ć":"Ć","Č":"Č","Ĉ":"Ĉ","Ċ":"Ċ","Ď":"Ď","Ḋ":"Ḋ","É":"É","È":"È","Ë":"Ë","Ẽ":"Ẽ","Ē":"Ē","Ḗ":"Ḗ","Ḕ":"Ḕ","Ĕ":"Ĕ","Ě":"Ě","Ê":"Ê","Ế":"Ế","Ề":"Ề","Ễ":"Ễ","Ė":"Ė","Ḟ":"Ḟ","Ǵ":"Ǵ","Ḡ":"Ḡ","Ğ":"Ğ","Ǧ":"Ǧ","Ĝ":"Ĝ","Ġ":"Ġ","Ḧ":"Ḧ","Ȟ":"Ȟ","Ĥ":"Ĥ","Ḣ":"Ḣ","Í":"Í","Ì":"Ì","Ï":"Ï","Ḯ":"Ḯ","Ĩ":"Ĩ","Ī":"Ī","Ĭ":"Ĭ","Ǐ":"Ǐ","Î":"Î","İ":"İ","Ĵ":"Ĵ","Ḱ":"Ḱ","Ǩ":"Ǩ","Ĺ":"Ĺ","Ľ":"Ľ","Ḿ":"Ḿ","Ṁ":"Ṁ","Ń":"Ń","Ǹ":"Ǹ","Ñ":"Ñ","Ň":"Ň","Ṅ":"Ṅ","Ó":"Ó","Ò":"Ò","Ö":"Ö","Ȫ":"Ȫ","Õ":"Õ","Ṍ":"Ṍ","Ṏ":"Ṏ","Ȭ":"Ȭ","Ō":"Ō","Ṓ":"Ṓ","Ṑ":"Ṑ","Ŏ":"Ŏ","Ǒ":"Ǒ","Ô":"Ô","Ố":"Ố","Ồ":"Ồ","Ỗ":"Ỗ","Ȯ":"Ȯ","Ȱ":"Ȱ","Ő":"Ő","Ṕ":"Ṕ","Ṗ":"Ṗ","Ŕ":"Ŕ","Ř":"Ř","Ṙ":"Ṙ","Ś":"Ś","Ṥ":"Ṥ","Š":"Š","Ṧ":"Ṧ","Ŝ":"Ŝ","Ṡ":"Ṡ","Ť":"Ť","Ṫ":"Ṫ","Ú":"Ú","Ù":"Ù","Ü":"Ü","Ǘ":"Ǘ","Ǜ":"Ǜ","Ǖ":"Ǖ","Ǚ":"Ǚ","Ũ":"Ũ","Ṹ":"Ṹ","Ū":"Ū","Ṻ":"Ṻ","Ŭ":"Ŭ","Ǔ":"Ǔ","Û":"Û","Ů":"Ů","Ű":"Ű","Ṽ":"Ṽ","Ẃ":"Ẃ","Ẁ":"Ẁ","Ẅ":"Ẅ","Ŵ":"Ŵ","Ẇ":"Ẇ","Ẍ":"Ẍ","Ẋ":"Ẋ","Ý":"Ý","Ỳ":"Ỳ","Ÿ":"Ÿ","Ỹ":"Ỹ","Ȳ":"Ȳ","Ŷ":"Ŷ","Ẏ":"Ẏ","Ź":"Ź","Ž":"Ž","Ẑ":"Ẑ","Ż":"Ż","ά":"ά","ὰ":"ὰ","ᾱ":"ᾱ","ᾰ":"ᾰ","έ":"έ","ὲ":"ὲ","ή":"ή","ὴ":"ὴ","ί":"ί","ὶ":"ὶ","ϊ":"ϊ","ΐ":"ΐ","ῒ":"ῒ","ῑ":"ῑ","ῐ":"ῐ","ό":"ό","ὸ":"ὸ","ύ":"ύ","ὺ":"ὺ","ϋ":"ϋ","ΰ":"ΰ","ῢ":"ῢ","ῡ":"ῡ","ῠ":"ῠ","ώ":"ώ","ὼ":"ὼ","Ύ":"Ύ","Ὺ":"Ὺ","Ϋ":"Ϋ","Ῡ":"Ῡ","Ῠ":"Ῠ","Ώ":"Ώ","Ὼ":"Ὼ"},ka=function(){function t(t,e){this.mode=void 0,this.gullet=void 0,this.settings=void 0,this.leftrightDepth=void 0,this.nextToken=void 0,this.mode="math",this.gullet=new ba(t,e,this.mode),this.settings=e,this.leftrightDepth=0}var e=t.prototype;return e.expect=function(t,e){if(void 0===e&&(e=!0),this.fetch().text!==t)throw new o("Expected '"+t+"', got '"+this.fetch().text+"'",this.fetch());e&&this.consume()},e.consume=function(){this.nextToken=null},e.fetch=function(){return null==this.nextToken&&(this.nextToken=this.gullet.expandNextToken()),this.nextToken},e.switchMode=function(t){this.mode=t,this.gullet.switchMode(t)},e.parse=function(){this.gullet.beginGroup(),this.settings.colorIsTextColor&&this.gullet.macros.set("\\color","\\textcolor");var t=this.parseExpression(!1);return this.expect("EOF"),this.gullet.endGroup(),t},e.parseExpression=function(e,r){for(var a=[];;){"math"===this.mode&&this.consumeSpaces();var n=this.fetch();if(-1!==t.endOfExpression.indexOf(n.text))break;if(r&&n.text===r)break;if(e&&na[n.text]&&na[n.text].infix)break;var i=this.parseAtom(r);if(!i)break;a.push(i)}return"text"===this.mode&&this.formLigatures(a),this.handleInfixNodes(a)},e.handleInfixNodes=function(t){for(var e,r=-1,a=0;a0&&!l||0===s&&!l&&"math"===this.mode,c=this.parseGroupOfType("argument to '"+t+"'",h,l,a,m);if(!c){if(l){i.push(null);continue}throw new o("Expected group after '"+t+"'",this.fetch())}(l?i:n).push(c)}return{args:n,optArgs:i}},e.parseGroupOfType=function(t,e,r,a,n){switch(e){case"color":return n&&this.consumeSpaces(),this.parseColorGroup(r);case"size":return n&&this.consumeSpaces(),this.parseSizeGroup(r);case"url":return this.parseUrlGroup(r,n);case"math":case"text":return this.parseGroup(t,r,a,void 0,e,n);case"hbox":var i=this.parseGroup(t,r,a,void 0,"text",n);return i?{type:"styling",mode:i.mode,body:[i],style:"text"}:i;case"raw":if(n&&this.consumeSpaces(),r&&"{"===this.fetch().text)return null;var s=this.parseStringGroup("raw",r,!0);if(s)return{type:"raw",mode:"text",string:s.text};throw new o("Expected raw group",this.fetch());case"original":case null:case void 0:return this.parseGroup(t,r,a,void 0,void 0,n);default:throw new o("Unknown group type as "+t,this.fetch())}},e.consumeSpaces=function(){for(;" "===this.fetch().text;)this.consume()},e.parseStringGroup=function(t,e,r){var a=e?"[":"{",n=e?"]":"}",i=this.fetch();if(i.text!==a){if(e)return null;if(r&&"EOF"!==i.text&&/[^{}[\]]/.test(i.text))return this.consume(),i}var s=this.mode;this.mode="text",this.expect(a);for(var h,l="",m=this.fetch(),c=0,u=m;(h=this.fetch()).text!==n||r&&c>0;){switch(h.text){case"EOF":throw new o("Unexpected end of input in "+t,m.range(u,l));case a:c++;break;case n:c--}l+=(u=h).text,this.consume()}return this.expect(n),this.mode=s,m.range(u,l)},e.parseRegexGroup=function(t,e){var r=this.mode;this.mode="text";for(var a,n=this.fetch(),i=n,s="";"EOF"!==(a=this.fetch()).text&&t.test(s+a.text);)s+=(i=a).text,this.consume();if(""===s)throw new o("Invalid "+e+": '"+n.text+"'",n);return this.mode=r,n.range(i,s)},e.parseColorGroup=function(t){var e=this.parseStringGroup("color",t);if(!e)return null;var r=/^(#[a-f0-9]{3}|#?[a-f0-9]{6}|[a-z]+)$/i.exec(e.text);if(!r)throw new o("Invalid color: '"+e.text+"'",e);var a=r[0];return/^[0-9a-f]{6}$/i.test(a)&&(a="#"+a),{type:"color-token",mode:this.mode,color:a}},e.parseSizeGroup=function(t){var e,r=!1;if(!(e=t||"{"===this.fetch().text?this.parseStringGroup("size",t):this.parseRegexGroup(/^[-+]? *(?:$|\d+|\d+\.\d*|\.\d*) *[a-z]{0,2} *$/,"size")))return null;t||0!==e.text.length||(e.text="0pt",r=!0);var a=/([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/.exec(e.text);if(!a)throw new o("Invalid size: '"+e.text+"'",e);var n={number:+(a[1]+a[2]),unit:a[3]};if(!At(n))throw new o("Invalid unit: '"+n.unit+"'",e);return{type:"size",mode:this.mode,value:n,isBlank:r}},e.parseUrlGroup=function(t,e){this.gullet.lexer.setCatcode("%",13);var r=this.parseStringGroup("url",t,!0);if(this.gullet.lexer.setCatcode("%",14),!r)return null;var a=r.text.replace(/\\([#$%&~_^{}])/g,"$1");return{type:"url",mode:this.mode,url:a}},e.parseGroup=function(e,r,n,i,s,h){var l=this.mode;s&&this.switchMode(s),h&&this.consumeSpaces();var m,c=this.fetch(),u=c.text;if(r?"["===u:"{"===u||"\\begingroup"===u){this.consume();var p=t.endOfGroup[u];this.gullet.beginGroup();var d=this.parseExpression(!1,p),f=this.fetch();this.expect(p),this.gullet.endGroup(),m={type:"ordgroup",mode:this.mode,loc:a.range(c,f),body:d,semisimple:"\\begingroup"===u||void 0}}else if(r)m=null;else if(null==(m=this.parseFunction(i,e,n)||this.parseSymbol())&&"\\"===u[0]&&!va.hasOwnProperty(u)){if(this.settings.throwOnError)throw new o("Undefined control sequence: "+u,c);m=this.formatUnsupportedCmd(u),this.consume()}return s&&this.switchMode(l),m},e.formLigatures=function(t){for(var e=t.length-1,r=0;r=0&&this.settings.reportNonstrict("unicodeTextInMathMode",'Latin-1/Unicode text character "'+e[0]+'" used in math mode',t);var h,l=$[this.mode][e].group,m=a.range(t);if(_.hasOwnProperty(l)){var c=l;h={type:"atom",mode:this.mode,family:c,loc:m,text:e}}else h={type:l,mode:this.mode,loc:m,text:e};i=h}else{if(!(e.charCodeAt(0)>=128))return null;this.settings.strict&&(M(e.charCodeAt(0))?"math"===this.mode&&this.settings.reportNonstrict("unicodeTextInMathMode",'Unicode text character "'+e[0]+'" used in math mode',t):this.settings.reportNonstrict("unknownSymbol",'Unrecognized Unicode character "'+e[0]+'" ('+e.charCodeAt(0)+")",t)),i={type:"textord",mode:"text",loc:a.range(t),text:e}}if(this.consume(),s)for(var u=0;u/g,p=//g;$docsify.plugins=[].concat((function(t){t.beforeEach(t=>{let e=t.replace(/(.*)<\/code>/g,(function(t,e){return`${e.replace(/`/g,"c194a9ec")}`})).replace(/\$`\$/g,"c194a9ed").replace(/\\`\{/g,"c194a9ee").replace(/\\\$/g,"c194a9eb").replace(/`[^`]*`/g,(function(t){return t.replace(/\$/g,"c194a9ef")})).replace(h,"`");return e=e.replace(l,"$ `$").replace(m,"\\`{"),e=e.replace(/(\$\$)([\s\S]*?)(\$\$)/g,(function(t,e,r){return"\x3c!-- begin-block-katex"+r+"end-block-katex--\x3e"})).replace(/(\$)([\s\S]*?)(\$)/g,(function(t,e,r){return"c194a9eg\x3c!-- begin-inline-katex"+r.replace(s,"\\$")+"end-inline-katex--\x3e"})).replace(s,"\\$"),e}),t.afterEach((function(t,e){let r=t.replace(u,(function(t,e){return n.a.renderToString(e,i)}));r=r.replace(p,(function(t,e){return n.a.renderToString(e,o)})),e(r.replace(c,"$"))}))}),$docsify.plugins)}]);
================================================
FILE: asset/docsify-quick-page.css
================================================
#prev-page-button {
position:fixed;
top:140px;
width: 35px;
height: 35px;
right: 15px;
background-color: transparent;
background-image: url(left.svg);
background-repeat: no-repeat;
background-size: cover;
border:0;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
outline:none;
cursor: pointer;
}
#next-page-button {
position:fixed;
top:180px;
width:35px;
height:35px;
right:15px;
background-color: transparent;
background-image: url(right.svg);
background-repeat: no-repeat;
background-size: cover;
border:0;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
outline:none;
cursor: pointer;
}
================================================
FILE: asset/docsify-quick-page.js
================================================
document.addEventListener('DOMContentLoaded', function() {
var prevBtn = document.createElement("div")
prevBtn.id = "prev-page-button"
document.body.appendChild(prevBtn)
var nextBtn = document.createElement("div");
nextBtn.id = "next-page-button"
document.body.appendChild(nextBtn)
var links = null
var linkMap = null
var getCurIdx = function() {
if (!links) {
links = Array
.from(document.querySelectorAll(".sidebar-nav a"))
.map(x => x.href)
linkMap = {}
links.forEach((x, i) => linkMap[x] = i)
}
var elem = document.querySelector(".active a")
var curIdx = elem? linkMap[elem.href]: -1
return curIdx
}
prevBtn.addEventListener('click', function () {
if (!document.body.classList.contains('ready'))
return
var curIdx = getCurIdx()
location.href = curIdx == -1?
links[0]:
links[(curIdx - 1 + links.length) % links.length]
document.body.scrollIntoView()
}, false)
nextBtn.addEventListener('click', function () {
if (!document.body.classList.contains('ready'))
return
var curIdx = getCurIdx()
location.href = links[(curIdx + 1) % links.length]
document.body.scrollIntoView()
}, false)
})
================================================
FILE: asset/edit.css
================================================
#edit-btn {
position: fixed;
right: 15px;
top: 260px;
width: 35px;
height: 35px;
background-repeat: no-repeat;
background-size: cover;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-image: url(edit.svg);
}
================================================
FILE: asset/edit.js
================================================
document.addEventListener('DOMContentLoaded', function() {
var editBtn = document.createElement('div')
editBtn.id = 'edit-btn'
document.body.append(editBtn)
var repo = window.$docsify.repo
editBtn.addEventListener('click', function() {
if (!repo) return
if (!/https?:\/\//.exec(repo))
repo = 'https://github.com/' + repo
var url = repo + '/tree/master' +
location.hash.slice(1) + '.md'
window.open(url)
})
})
================================================
FILE: asset/prism-darcula.css
================================================
/**
* Darcula theme
*
* Adapted from a theme based on:
* IntelliJ Darcula Theme (https://github.com/bulenkov/Darcula)
*
* @author Alexandre Paradis
* @version 1.0
*/
code[class*="lang-"],
pre[data-lang] {
color: #a9b7c6 !important;
background-color: #2b2b2b !important;
font-family: Consolas, Monaco, 'Andale Mono', monospace;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[data-lang]::-moz-selection, pre[data-lang] ::-moz-selection,
code[class*="lang-"]::-moz-selection, code[class*="lang-"] ::-moz-selection {
color: inherit;
background: rgba(33, 66, 131, .85);
}
pre[data-lang]::selection, pre[data-lang] ::selection,
code[class*="lang-"]::selection, code[class*="lang-"] ::selection {
color: inherit;
background: rgba(33, 66, 131, .85);
}
/* Code blocks */
pre[data-lang] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre) > code[class*="lang-"],
pre[data-lang] {
background: #2b2b2b;
}
/* Inline code */
:not(pre) > code[class*="lang-"] {
padding: .1em;
border-radius: .3em;
}
.token.comment,
.token.prolog,
.token.cdata {
color: #808080;
}
.token.delimiter,
.token.boolean,
.token.keyword,
.token.selector,
.token.important,
.token.atrule {
color: #cc7832;
}
.token.operator,
.token.punctuation,
.token.attr-name {
color: #a9b7c6;
}
.token.tag,
.token.tag .punctuation,
.token.doctype,
.token.builtin {
color: #e8bf6a;
}
.token.entity,
.token.number,
.token.symbol {
color: #6897bb;
}
.token.property,
.token.constant,
.token.variable {
color: #9876aa;
}
.token.string,
.token.char {
color: #6a8759;
}
.token.attr-value,
.token.attr-value .punctuation {
color: #a5c261;
}
.token.attr-value .punctuation:first-child {
color: #a9b7c6;
}
.token.url {
color: #287bde;
text-decoration: underline;
}
.token.function {
color: #ffc66d;
}
.token.regex {
background: #364135;
}
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.inserted {
background: #294436;
}
.token.deleted {
background: #484a4a;
}
code.lang-css .token.property,
code.lang-css .token.property + .token.punctuation {
color: #a9b7c6;
}
code.lang-css .token.id {
color: #ffc66d;
}
code.lang-css .token.selector > .token.class,
code.lang-css .token.selector > .token.attribute,
code.lang-css .token.selector > .token.pseudo-class,
code.lang-css .token.selector > .token.pseudo-element {
color: #ffc66d;
}
================================================
FILE: asset/share.css
================================================
#share-btn {
position: fixed;
right: 15px;
top: 220px;
width: 35px;
height: 35px;
background-repeat: no-repeat;
background-size: cover;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-image: url('share.svg');
}
================================================
FILE: asset/share.js
================================================
document.addEventListener('DOMContentLoaded', function() {
var shareBtn = document.createElement('a')
shareBtn.id = 'share-btn'
shareBtn.className = 'bdsharebuttonbox'
shareBtn.setAttribute('data-cmd', 'more')
document.body.append(shareBtn)
window._bd_share_config = {
"common":{
"bdSnsKey":{},
"bdText":"",
"bdMini":"1",
"bdMiniList":false,
"bdPic":"",
"bdStyle":"2",
"bdSize":"16"
},
"share":{}
}
})
// https://bdimg.share.baidu.com/static/api/js/share.js
window._bd_share_main?window._bd_share_is_recently_loaded=!0:(window._bd_share_is_recently_loaded=!1,window._bd_share_main={version:"2.0",jscfg:{domain:{staticUrl:"https://bdimg.share.baidu.com/"}}}),!window._bd_share_is_recently_loaded&&(window._bd_share_main.F=window._bd_share_main.F||function(e,t){function r(e,t){if(e instanceof Array){for(var n=0,r=e.length;n1?(this.svnMod=n[0],this.name=n[1]):this.name=t}this.svnMod||(this.svnMod=this.path.split("/js/")[0].substr(1)),this.type="js",this.getKey=function(){return this.svnMod+":"+this.name},this._info={}}function o(e,t){var n=t=="css",r=document.createElement(n?"link":"script");return r}function u(t,n,r,i){function c(){c.isCalled||(c.isCalled=!0,clearTimeout(l),r&&r())}var s=o(t,n);s.nodeName==="SCRIPT"?a(s,c):f(s,c);var l=setTimeout(function(){throw new Error("load "+n+" timeout : "+t)},e._loadScriptTimeout||1e4),h=document.getElementsByTagName("head")[0];n=="css"?(s.rel="stylesheet",s.href=t,h.appendChild(s)):(s.type="text/javascript",s.src=t,h.insertBefore(s,h.firstChild))}function a(e,t){e.onload=e.onerror=e.onreadystatechange=function(){if(/loaded|complete|undefined/.test(e.readyState)){e.onload=e.onerror=e.onreadystatechange=null;if(e.parentNode){e.parentNode.removeChild(e);try{if(e.clearAttributes)e.clearAttributes();else for(var n in e)delete e[n]}catch(r){}}e=undefined,t&&t()}}}function f(e,t){e.attachEvent?e.attachEvent("onload",t):setTimeout(function(){l(e,t)},0)}function l(e,t){if(t&&t.isCalled)return;var n,r=navigator.userAgent,i=~r.indexOf("AppleWebKit"),s=~r.indexOf("Opera");if(i||s)e.sheet&&(n=!0);else if(e.sheet)try{e.sheet.cssRules&&(n=!0)}catch(o){if(o.name==="SecurityError"||o.name==="NS_ERROR_DOM_SECURITY_ERR")n=!0}setTimeout(function(){n?t&&t():l(e,t)},1)}var n="api";e.each=r,i.currentPath="",i.loadedPaths={},i.loadingPaths={},i.cache={},i.paths={},i.handlers=[],i.moduleFileMap={},i.requiredPaths={},i.lazyLoadPaths={},i.services={},i.isPathsLoaded=function(e){var t=!0;return r(e,function(e){if(!(e in i.loadedPaths))return t=!1}),t},i.require=function(e,t){e.search(":")<0&&(t||(t=n,i.currentPath&&(t=i.currentPath.split("/js/")[0].substr(1))),e=t+":"+e);var r=i.get(e,i.currentPath);if(r.type=="css")return;if(r){if(!r._inited){r._inited=!0;var s,o=r.svnMod;if(s=r.fn.call(null,function(e){return i.require(e,o)},r.exports,new h(r.name,o)))r.exports=s}return r.exports}throw new Error('Module "'+e+'" not found!')},i.baseUrl=t?t[t.length-1]=="/"?t:t+"/":"/",i.getBasePath=function(e){var t,n;return(n=e.indexOf("/"))!==-1&&(t=e.slice(0,n)),t&&t in i.paths?i.paths[t]:i.baseUrl},i.getJsPath=function(t,r){if(t.charAt(0)==="."){r=r.replace(/\/[^\/]+\/[^\/]+$/,""),t.search("./")===0&&(t=t.substr(2));var s=0;t=t.replace(/^(\.\.\/)+/g,function(e){return s=e.length/3,""});while(s>0)r=r.substr(0,r.lastIndexOf("/")),s--;return r+"/"+t+"/"+t.substr(t.lastIndexOf("/")+1)+".js"}var o,u,a,f,l,c;if(t.search(":")>=0){var h=t.split(":");o=h[0],t=h[1]}else r&&(o=r.split("/")[1]);o=o||n;var p=/\.css(?:\?|$)/i.test(t);p&&e._useConfig&&i.moduleFileMap[o][t]&&(t=i.moduleFileMap[o][t]);var t=l=t,d=i.getBasePath(t);return(a=t.indexOf("/"))!==-1&&(u=t.slice(0,a),f=t.lastIndexOf("/"),l=t.slice(f+1)),u&&u in i.paths&&(t=t.slice(a+1)),c=d+o+"/js/"+t+".js",c},i.get=function(e,t){var n=i.getJsPath(e,t);return i.cache[n]?i.cache[n]:new i(n,e)},i.prototype={load:function(){i.loadingPaths[this.path]=!0;var t=this.svnMod||n,r=window._bd_share_main.jscfg.domain.staticUrl+"static/"+t+"/",o=this,u=/\.css(?:\?|$)/i.test(this.name);this.type=u?"css":"js";var a="/"+this.type+"/"+i.moduleFileMap[t][this.name];e._useConfig&&i.moduleFileMap[t][this.name]?r+=this.type+"/"+i.moduleFileMap[t][this.name]:r+=this.type+"/"+this.name+(u?"":".js");if(e._firstScreenCSS.indexOf(this.name)>0||e._useConfig&&a==e._firstScreenJS)o._loaded=!0,o.ready();else{var f=(new Date).getTime();s.create({src:r,type:this.type,loaded:function(){o._info.loadedTime=(new Date).getTime()-f,o.type=="css"&&(o._loaded=!0,o.ready())}})}},lazyLoad:function(){var e=this.name;if(i.lazyLoadPaths[this.getKey()])this.define(),delete i.lazyLoadPaths[this.getKey()];else{if(this.exist())return;i.requiredPaths[this.getKey()]=!0,this.load()}},ready:function(e,t){var n=t?this._requiredStack:this._readyStack;if(e)this._loaded?e():n.push(e);else{i.loadedPaths[this.path]=!0,delete i.loadingPaths[this.path],this._loaded=!0,i.currentPath=this.path;if(this._readyStack&&this._readyStack.length>0){this._inited=!0;var s,o=this.svnMod;this.fn&&(s=this.fn.call(null,function(e){return i.require(e,o)},this.exports,new h(this.name,o)))&&(this.exports=s),r(this._readyStack,function(e){e()}),delete this._readyStack}this._requiredStack&&this._requiredStack.length>0&&(r(this._requiredStack,function(e){e()}),delete this._requiredStack)}},define:function(){var e=this,t=this.deps,n=this.path,s=[];t||(t=this.getDependents()),t.length?(r(t,function(t){s.push(i.getJsPath(t,e.path))}),r(t,function(t){var n=i.get(t,e.path);n.ready(function(){i.isPathsLoaded(s)&&e.ready()},!0),n.lazyLoad()})):this.ready()},exist:function(){var e=this.path;return e in i.loadedPaths||e in i.loadingPaths},getDependents:function(){var e=this,t=this.fn.toString(),n=t.match(/function\s*\(([^,]*),/i),i=new RegExp("[^.]\\b"+n[1]+"\\(\\s*('|\")([^()\"']*)('|\")\\s*\\)","g"),s=t.match(i),o=[];return s&&r(s,function(e,t){o[t]=e.substr(n[1].length+3).slice(0,-2)}),o}};var s={create:function(e){var t=e.src;if(t in this._paths)return;this._paths[t]=!0,r(this._rules,function(e){t=e.call(null,t)}),u(t,e.type,e.loaded)},_paths:{},_rules:[],addPathRule:function(e){this._rules.push(e)}};e.version="1.0",e.use=function(e,t){typeof e=="string"&&(e=[e]);var n=[],s=[];r(e,function(e,t){s[t]=!1}),r(e,function(e,o){var u=i.get(e),a=u._loaded;u.ready(function(){var e=u.exports||{};e._INFO=u._info,e._INFO&&(e._INFO.isNew=!a),n[o]=e,s[o]=!0;var i=!0;r(s,function(e){if(e===!1)return i=!1}),t&&i&&t.apply(null,n)}),u.lazyLoad()})},e.module=function(e,t,n){var r=i.get(e);r.fn=t,r.deps=n,i.requiredPaths[r.getKey()]?r.define():i.lazyLoadPaths[r.getKey()]=!0},e.pathRule=function(e){s.addPathRule(e)},e._addPath=function(e,t){t.slice(-1)!=="/"&&(t+="/");if(e in i.paths)throw new Error(e+" has already in Module.paths");i.paths[e]=t};var c=n;e._setMod=function(e){c=e||n},e._fileMap=function(t,n){if(typeof t=="object")r(t,function(t,n){e._fileMap(n,t)});else{var s=c;typeof n=="string"&&(n=[n]),t=t.indexOf("js/")==1?t.substr(4):t,t=t.indexOf("css/")==1?t.substr(5):t;var o=i.moduleFileMap[s];o||(o={}),r(n,function(e){o[e]||(o[e]=t)}),i.moduleFileMap[s]=o}},e._eventMap={},e.call=function(t,n,r){var i=[];for(var s=2,o=arguments.length;s=0;r--)t[r]=this.svnMod+":"+t[r];e.use(t,n)}},e._Context=h,e.addLog=function(t,n){e.use("lib/log",function(e){e.defaultLog(t,n)})},e.fire=function(t,n,r){e.use("lib/mod_evt",function(e){e.fire(t,n,r)})},e._defService=function(e,t){if(e){var n=i.services[e];n=n||{},r(t,function(e,t){n[t]=e}),i.services[e]=n}},e.getService=function(t,n,r){var s=i.services[t];if(!s)throw new Error(t+" mod didn't define any services");var o=s[n];if(!o)throw new Error(t+" mod didn't provide service "+n);e.use(t+":"+o,r)},e}({})),!window._bd_share_is_recently_loaded&&window._bd_share_main.F.module("base/min_tangram",function(e,t){var n={};n.each=function(e,t,n){var r,i,s,o=e.length;if("function"==typeof t)for(s=0;s0?t.each(e[o],function(s,u){e[o][s]=t.extend({},r[o],n,u,i[o])}):e[o]=t.extend({},r[o],n,e[o],i[o]))}),e}var t=e.T;_bd_share_main.init=function(e){e=e||window._bd_share_config||{share:{}};if(e){var t=i(e);t.like&&r(["share/like_api","view/like_view"],t.like),t.share&&r(["share/share_api","view/share_view"],t.share),t.slide&&r(["share/slide_api","view/slide_view"],t.slide),t.selectShare&&r(["share/select_api","view/select_view"],t.selectShare),t.image&&r(["share/image_api","view/image_view"],t.image)}},window._bd_share_main._LogPoolV2=[],window._bd_share_main.n1=(new Date).getTime(),t.domready(function(){window._bd_share_main.n2=(new Date).getTime()+1e3,_bd_share_main.init(),setTimeout(function(){window._bd_share_main.F.use("trans/logger",function(e){e.nsClick(),e.back(),e.duration()})},3e3)})}),!window._bd_share_is_recently_loaded&&window._bd_share_main.F.module("component/comm_tools",function(e,t){var n=function(){var e=window.location||document.location||{};return e.href||""},r=function(e,t){var n=e.length,r="";for(var i=1;i<=t;i++){var s=Math.floor(n*Math.random());r+=e.charAt(s)}return r},i=function(){var e=(+(new Date)).toString(36),t=r("0123456789abcdefghijklmnopqrstuvwxyz",3);return e+t};t.getLinkId=i,t.getPageUrl=n}),!window._bd_share_is_recently_loaded&&window._bd_share_main.F.module("trans/trans",function(e,t){var n=e("component/comm_tools"),r=e("conf/const").URLS,i=function(){window._bd_share_main.F.use("base/tangram",function(e){var t=e.T;t.cookie.get("bdshare_firstime")==null&&t.cookie.set("bdshare_firstime",new Date*1,{path:"/",expires:(new Date).setFullYear(2022)-new Date})})},s=function(e){var t=e.bdUrl||n.getPageUrl();return t=t.replace(/\'/g,"%27").replace(/\"/g,"%22"),t},o=function(e){var t=(new Date).getTime()+3e3,r={click:1,url:s(e),uid:e.bdUid||"0",to:e.__cmd,type:"text",pic:e.bdPic||"",title:(e.bdText||document.title).substr(0,300),key:(e.bdSnsKey||{})[e.__cmd]||"",desc:e.bdDesc||"",comment:e.bdComment||"",relateUid:e.bdWbuid||"",searchPic:e.bdSearchPic||0,sign:e.bdSign||"on",l:window._bd_share_main.n1.toString(32)+window._bd_share_main.n2.toString(32)+t.toString(32),linkid:n.getLinkId(),firstime:a("bdshare_firstime")||""};switch(e.__cmd){case"copy":l(r);break;case"print":c();break;case"bdxc":h();break;case"bdysc":p(r);break;case"weixin":d(r);break;default:u(e,r)}window._bd_share_main.F.use("trans/logger",function(t){t.commit(e,r)})},u=function(e,t){var n=r.jumpUrl;e.__cmd=="mshare"?n=r.mshareUrl:e.__cmd=="mail"&&(n=r.emailUrl);var i=n+"?"+f(t);window.open(i)},a=function(e){if(e){var t=new RegExp("(^| )"+e+"=([^;]*)(;|$)"),n=t.exec(document.cookie);if(n)return decodeURIComponent(n[2]||null)}},f=function(e){var t=[];for(var n in e)t.push(encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return t.join("&").replace(/%20/g,"+")},l=function(e){window._bd_share_main.F.use("base/tangram",function(t){var r=t.T;r.browser.ie?(window.clipboardData.setData("text",document.title+" "+(e.bdUrl||n.getPageUrl())),alert("\u6807\u9898\u548c\u94fe\u63a5\u590d\u5236\u6210\u529f\uff0c\u60a8\u53ef\u4ee5\u63a8\u8350\u7ed9QQ/MSN\u4e0a\u7684\u597d\u53cb\u4e86\uff01")):window.prompt("\u60a8\u4f7f\u7528\u7684\u662f\u975eIE\u6838\u5fc3\u6d4f\u89c8\u5668\uff0c\u8bf7\u6309\u4e0b Ctrl+C \u590d\u5236\u4ee3\u7801\u5230\u526a\u8d34\u677f",document.title+" "+(e.bdUrl||n.getPageUrl()))})},c=function(){window.print()},h=function(){window._bd_share_main.F.use("trans/trans_bdxc",function(e){e&&e.run()})},p=function(e){window._bd_share_main.F.use("trans/trans_bdysc",function(t){t&&t.run(e)})},d=function(e){window._bd_share_main.F.use("trans/trans_weixin",function(t){t&&t.run(e)})},v=function(e){o(e)};t.run=v,i()});
================================================
FILE: asset/style.css
================================================
.markdown-section h1 {
margin: 3rem 0 2rem 0;
}
.markdown-section h2 {
margin: 2rem 0 1rem;
}
img,
pre {
border-radius: 8px;
}
.content,
.sidebar,
.markdown-section,
body,
.search input {
background-color: rgba(243, 242, 238, 1) !important;
}
@media (min-width:600px) {
.sidebar-toggle {
background-color: #f3f2ee;
}
}
.docsify-copy-code-button {
background: #f8f8f8 !important;
color: #7a7a7a !important;
}
body {
/*font-family: Microsoft YaHei, Source Sans Pro, Helvetica Neue, Arial, sans-serif !important;*/
}
.markdown-section>p {
font-size: 16px !important;
}
.markdown-section pre>code {
font-family: Consolas, Roboto Mono, Monaco, courier, monospace !important;
font-size: .9rem !important;
}
/*.anchor span {
color: rgb(66, 185, 131);
}*/
section.cover h1 {
margin: 0;
}
body>section>div.cover-main>ul>li>a {
color: #42b983;
}
.markdown-section img {
box-shadow: 7px 9px 10px #aaa !important;
}
pre {
background-color: #f3f2ee !important;
}
@media (min-width:600px) {
pre code {
/*box-shadow: 2px 1px 20px 2px #aaa;*/
/*border-radius: 10px !important;*/
padding-left: 20px !important;
}
}
@media (max-width:600px) {
pre {
padding-left: 0px !important;
padding-right: 0px !important;
}
}
.markdown-section pre {
padding-left: 0 !important;
padding-right: 0px !important;
box-shadow: 2px 1px 20px 2px #aaa;
}
iframe {
display: inline;
}
================================================
FILE: asset/vue.css
================================================
@import url("https://fonts.googleapis.com/css?family=Roboto+Mono|Source+Sans+Pro:300,400,600");
* {
-webkit-font-smoothing: antialiased;
-webkit-overflow-scrolling: touch;
-webkit-tap-highlight-color: rgba(0,0,0,0);
-webkit-text-size-adjust: none;
-webkit-touch-callout: none;
box-sizing: border-box;
}
body:not(.ready) {
overflow: hidden;
}
body:not(.ready) [data-cloak],
body:not(.ready) .app-nav,
body:not(.ready) > nav {
display: none;
}
div#app {
font-size: 30px;
font-weight: lighter;
margin: 40vh auto;
text-align: center;
}
div#app:empty::before {
content: 'Loading...';
}
.emoji {
height: 1.2rem;
vertical-align: middle;
}
.progress {
background-color: var(--theme-color, #42b983);
height: 2px;
left: 0px;
position: fixed;
right: 0px;
top: 0px;
transition: width 0.2s, opacity 0.4s;
width: 0%;
z-index: 999999;
}
.search a:hover {
color: var(--theme-color, #42b983);
}
.search .search-keyword {
color: var(--theme-color, #42b983);
font-style: normal;
font-weight: bold;
}
html,
body {
height: 100%;
}
body {
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
color: #34495e;
font-family: 'Source Sans Pro', 'Helvetica Neue', Arial, sans-serif;
font-size: 15px;
letter-spacing: 0;
margin: 0;
overflow-x: hidden;
}
img {
max-width: 100%;
}
a[disabled] {
cursor: not-allowed;
opacity: 0.6;
}
kbd {
border: solid 1px #ccc;
border-radius: 3px;
display: inline-block;
font-size: 12px !important;
line-height: 12px;
margin-bottom: 3px;
padding: 3px 5px;
vertical-align: middle;
}
li input[type='checkbox'] {
margin: 0 0.2em 0.25em 0;
vertical-align: middle;
}
.app-nav {
margin: 25px 60px 0 0;
position: absolute;
right: 0;
text-align: right;
z-index: 10;
/* navbar dropdown */
}
.app-nav.no-badge {
margin-right: 25px;
}
.app-nav p {
margin: 0;
}
.app-nav > a {
margin: 0 1rem;
padding: 5px 0;
}
.app-nav ul,
.app-nav li {
display: inline-block;
list-style: none;
margin: 0;
}
.app-nav a {
color: inherit;
font-size: 16px;
text-decoration: none;
transition: color 0.3s;
}
.app-nav a:hover {
color: var(--theme-color, #42b983);
}
.app-nav a.active {
border-bottom: 2px solid var(--theme-color, #42b983);
color: var(--theme-color, #42b983);
}
.app-nav li {
display: inline-block;
margin: 0 1rem;
padding: 5px 0;
position: relative;
cursor: pointer;
}
.app-nav li ul {
background-color: #fff;
border: 1px solid #ddd;
border-bottom-color: #ccc;
border-radius: 4px;
box-sizing: border-box;
display: none;
max-height: calc(100vh - 61px);
overflow-y: auto;
padding: 10px 0;
position: absolute;
right: -15px;
text-align: left;
top: 100%;
white-space: nowrap;
}
.app-nav li ul li {
display: block;
font-size: 14px;
line-height: 1rem;
margin: 0;
margin: 8px 14px;
white-space: nowrap;
}
.app-nav li ul a {
display: block;
font-size: inherit;
margin: 0;
padding: 0;
}
.app-nav li ul a.active {
border-bottom: 0;
}
.app-nav li:hover ul {
display: block;
}
.github-corner {
border-bottom: 0;
position: fixed;
right: 0;
text-decoration: none;
top: 0;
z-index: 1;
}
.github-corner:hover .octo-arm {
-webkit-animation: octocat-wave 560ms ease-in-out;
animation: octocat-wave 560ms ease-in-out;
}
.github-corner svg {
color: #fff;
fill: var(--theme-color, #42b983);
height: 80px;
width: 80px;
}
main {
display: block;
position: relative;
width: 100vw;
height: 100%;
z-index: 0;
}
main.hidden {
display: none;
}
.anchor {
display: inline-block;
text-decoration: none;
transition: all 0.3s;
}
.anchor span {
color: #34495e;
}
.anchor:hover {
text-decoration: underline;
}
.sidebar {
border-right: 1px solid rgba(0,0,0,0.07);
overflow-y: auto;
padding: 40px 0 0;
position: absolute;
top: 0;
bottom: 0;
left: 0;
transition: transform 250ms ease-out;
width: 300px;
z-index: 20;
}
.sidebar > h1 {
margin: 0 auto 1rem;
font-size: 1.5rem;
font-weight: 300;
text-align: center;
}
.sidebar > h1 a {
color: inherit;
text-decoration: none;
}
.sidebar > h1 .app-nav {
display: block;
position: static;
}
.sidebar .sidebar-nav {
line-height: 2em;
padding-bottom: 40px;
}
.sidebar li.collapse .app-sub-sidebar {
display: none;
}
.sidebar ul {
margin: 0 0 0 15px;
padding: 0;
}
.sidebar li > p {
font-weight: 700;
margin: 0;
}
.sidebar ul,
.sidebar ul li {
list-style: none;
}
.sidebar ul li a {
border-bottom: none;
display: block;
}
.sidebar ul li ul {
padding-left: 20px;
}
.sidebar::-webkit-scrollbar {
width: 4px;
}
.sidebar::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 4px;
}
.sidebar:hover::-webkit-scrollbar-thumb {
background: rgba(136,136,136,0.4);
}
.sidebar:hover::-webkit-scrollbar-track {
background: rgba(136,136,136,0.1);
}
.sidebar-toggle {
background-color: transparent;
background-color: rgba(255,255,255,0.8);
border: 0;
outline: none;
padding: 10px;
position: absolute;
bottom: 0;
left: 0;
text-align: center;
transition: opacity 0.3s;
width: 284px;
z-index: 30;
cursor: pointer;
}
.sidebar-toggle:hover .sidebar-toggle-button {
opacity: 0.4;
}
.sidebar-toggle span {
background-color: var(--theme-color, #42b983);
display: block;
margin-bottom: 4px;
width: 16px;
height: 2px;
}
body.sticky .sidebar,
body.sticky .sidebar-toggle {
position: fixed;
}
.content {
padding-top: 60px;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 300px;
transition: left 250ms ease;
}
.markdown-section {
margin: 0 auto;
max-width: 80%;
padding: 30px 15px 40px 15px;
position: relative;
}
.markdown-section > * {
box-sizing: border-box;
font-size: inherit;
}
.markdown-section > :first-child {
margin-top: 0 !important;
}
.markdown-section hr {
border: none;
border-bottom: 1px solid #eee;
margin: 2em 0;
}
.markdown-section iframe {
border: 1px solid #eee;
/* fix horizontal overflow on iOS Safari */
width: 1px;
min-width: 100%;
}
.markdown-section table {
border-collapse: collapse;
border-spacing: 0;
display: block;
margin-bottom: 1rem;
overflow: auto;
width: 100%;
}
.markdown-section th {
border: 1px solid #ddd;
font-weight: bold;
padding: 6px 13px;
}
.markdown-section td {
border: 1px solid #ddd;
padding: 6px 13px;
}
.markdown-section tr {
border-top: 1px solid #ccc;
}
.markdown-section tr:nth-child(2n) {
background-color: #f8f8f8;
}
.markdown-section p.tip {
background-color: #f8f8f8;
border-bottom-right-radius: 2px;
border-left: 4px solid #f66;
border-top-right-radius: 2px;
margin: 2em 0;
padding: 12px 24px 12px 30px;
position: relative;
}
.markdown-section p.tip:before {
background-color: #f66;
border-radius: 100%;
color: #fff;
content: '!';
font-family: 'Dosis', 'Source Sans Pro', 'Helvetica Neue', Arial, sans-serif;
font-size: 14px;
font-weight: bold;
left: -12px;
line-height: 20px;
position: absolute;
height: 20px;
width: 20px;
text-align: center;
top: 14px;
}
.markdown-section p.tip code {
background-color: #efefef;
}
.markdown-section p.tip em {
color: #34495e;
}
.markdown-section p.warn {
background: rgba(66,185,131,0.1);
border-radius: 2px;
padding: 1rem;
}
.markdown-section ul.task-list > li {
list-style-type: none;
}
body.close .sidebar {
transform: translateX(-300px);
}
body.close .sidebar-toggle {
width: auto;
}
body.close .content {
left: 0;
}
@media print {
.github-corner,
.sidebar-toggle,
.sidebar,
.app-nav {
display: none;
}
}
@media screen and (max-width: 768px) {
.github-corner,
.sidebar-toggle,
.sidebar {
position: fixed;
}
.app-nav {
margin-top: 16px;
}
.app-nav li ul {
top: 30px;
}
main {
height: auto;
overflow-x: hidden;
}
.sidebar {
left: -300px;
transition: transform 250ms ease-out;
}
.content {
left: 0;
max-width: 100vw;
position: static;
padding-top: 20px;
transition: transform 250ms ease;
}
.app-nav,
.github-corner {
transition: transform 250ms ease-out;
}
.sidebar-toggle {
background-color: transparent;
width: auto;
padding: 30px 30px 10px 10px;
}
body.close .sidebar {
transform: translateX(300px);
}
body.close .sidebar-toggle {
background-color: rgba(255,255,255,0.8);
transition: 1s background-color;
width: 284px;
padding: 10px;
}
body.close .content {
transform: translateX(300px);
}
body.close .app-nav,
body.close .github-corner {
display: none;
}
.github-corner:hover .octo-arm {
-webkit-animation: none;
animation: none;
}
.github-corner .octo-arm {
-webkit-animation: octocat-wave 560ms ease-in-out;
animation: octocat-wave 560ms ease-in-out;
}
}
@-webkit-keyframes octocat-wave {
0%, 100% {
transform: rotate(0);
}
20%, 60% {
transform: rotate(-25deg);
}
40%, 80% {
transform: rotate(10deg);
}
}
@keyframes octocat-wave {
0%, 100% {
transform: rotate(0);
}
20%, 60% {
transform: rotate(-25deg);
}
40%, 80% {
transform: rotate(10deg);
}
}
section.cover {
align-items: center;
background-position: center center;
background-repeat: no-repeat;
background-size: cover;
height: 100vh;
width: 100vw;
display: none;
}
section.cover.show {
display: flex;
}
section.cover.has-mask .mask {
background-color: #fff;
opacity: 0.8;
position: absolute;
top: 0;
height: 100%;
width: 100%;
}
section.cover .cover-main {
flex: 1;
margin: -20px 16px 0;
text-align: center;
position: relative;
}
section.cover a {
color: inherit;
text-decoration: none;
}
section.cover a:hover {
text-decoration: none;
}
section.cover p {
line-height: 1.5rem;
margin: 1em 0;
}
section.cover h1 {
color: inherit;
font-size: 2.5rem;
font-weight: 300;
margin: 0.625rem 0 2.5rem;
position: relative;
text-align: center;
}
section.cover h1 a {
display: block;
}
section.cover h1 small {
bottom: -0.4375rem;
font-size: 1rem;
position: absolute;
}
section.cover blockquote {
font-size: 1.5rem;
text-align: center;
}
section.cover ul {
line-height: 1.8;
list-style-type: none;
margin: 1em auto;
max-width: 500px;
padding: 0;
}
section.cover .cover-main > p:last-child a {
border-color: var(--theme-color, #42b983);
border-radius: 2rem;
border-style: solid;
border-width: 1px;
box-sizing: border-box;
color: var(--theme-color, #42b983);
display: inline-block;
font-size: 1.05rem;
letter-spacing: 0.1rem;
margin: 0.5rem 1rem;
padding: 0.75em 2rem;
text-decoration: none;
transition: all 0.15s ease;
}
section.cover .cover-main > p:last-child a:last-child {
background-color: var(--theme-color, #42b983);
color: #fff;
}
section.cover .cover-main > p:last-child a:last-child:hover {
color: inherit;
opacity: 0.8;
}
section.cover .cover-main > p:last-child a:hover {
color: inherit;
}
section.cover blockquote > p > a {
border-bottom: 2px solid var(--theme-color, #42b983);
transition: color 0.3s;
}
section.cover blockquote > p > a:hover {
color: var(--theme-color, #42b983);
}
body {
background-color: #fff;
}
/* sidebar */
.sidebar {
background-color: #fff;
color: #364149;
}
.sidebar li {
margin: 6px 0 6px 0;
}
.sidebar ul li a {
color: #505d6b;
font-size: 14px;
font-weight: normal;
overflow: hidden;
text-decoration: none;
text-overflow: ellipsis;
white-space: nowrap;
}
.sidebar ul li a:hover {
text-decoration: underline;
}
.sidebar ul li ul {
padding: 0;
}
.sidebar ul li.active > a {
border-right: 2px solid;
color: var(--theme-color, #42b983);
font-weight: 600;
}
.app-sub-sidebar li::before {
content: '-';
padding-right: 4px;
float: left;
}
/* markdown content found on pages */
.markdown-section h1,
.markdown-section h2,
.markdown-section h3,
.markdown-section h4,
.markdown-section strong {
color: #2c3e50;
font-weight: 600;
}
.markdown-section a {
color: var(--theme-color, #42b983);
font-weight: 600;
}
.markdown-section h1 {
font-size: 2rem;
margin: 0 0 1rem;
}
.markdown-section h2 {
font-size: 1.75rem;
margin: 45px 0 0.8rem;
}
.markdown-section h3 {
font-size: 1.5rem;
margin: 40px 0 0.6rem;
}
.markdown-section h4 {
font-size: 1.25rem;
}
.markdown-section h5 {
font-size: 1rem;
}
.markdown-section h6 {
color: #777;
font-size: 1rem;
}
.markdown-section figure,
.markdown-section p {
margin: 1.2em 0;
}
.markdown-section p,
.markdown-section ul,
.markdown-section ol {
line-height: 1.6rem;
word-spacing: 0.05rem;
}
.markdown-section ul,
.markdown-section ol {
padding-left: 1.5rem;
}
.markdown-section blockquote {
border-left: 4px solid var(--theme-color, #42b983);
color: #858585;
margin: 2em 0;
padding-left: 20px;
}
.markdown-section blockquote p {
font-weight: 600;
margin-left: 0;
}
.markdown-section iframe {
margin: 1em 0;
}
.markdown-section em {
color: #7f8c8d;
}
.markdown-section code {
background-color: #f8f8f8;
border-radius: 2px;
color: #e96900;
font-family: 'Roboto Mono', Monaco, courier, monospace;
font-size: 0.8rem;
margin: 0 2px;
padding: 3px 5px;
white-space: pre-wrap;
}
.markdown-section pre {
-moz-osx-font-smoothing: initial;
-webkit-font-smoothing: initial;
background-color: #f8f8f8;
font-family: 'Roboto Mono', Monaco, courier, monospace;
line-height: 1.5rem;
margin: 1.2em 0;
overflow: auto;
padding: 0 1.4rem;
position: relative;
word-wrap: normal;
}
/* code highlight */
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #8e908c;
}
.token.namespace {
opacity: 0.7;
}
.token.boolean,
.token.number {
color: #c76b29;
}
.token.punctuation {
color: #525252;
}
.token.property {
color: #c08b30;
}
.token.tag {
color: #2973b7;
}
.token.string {
color: var(--theme-color, #42b983);
}
.token.selector {
color: #6679cc;
}
.token.attr-name {
color: #2973b7;
}
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #22a2c9;
}
.token.attr-value,
.token.control,
.token.directive,
.token.unit {
color: var(--theme-color, #42b983);
}
.token.keyword,
.token.function {
color: #e96900;
}
.token.statement,
.token.regex,
.token.atrule {
color: #22a2c9;
}
.token.placeholder,
.token.variable {
color: #3d8fd1;
}
.token.deleted {
text-decoration: line-through;
}
.token.inserted {
border-bottom: 1px dotted #202746;
text-decoration: none;
}
.token.italic {
font-style: italic;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.important {
color: #c94922;
}
.token.entity {
cursor: help;
}
.markdown-section pre > code {
-moz-osx-font-smoothing: initial;
-webkit-font-smoothing: initial;
background-color: #f8f8f8;
border-radius: 2px;
color: #525252;
display: block;
font-family: 'Roboto Mono', Monaco, courier, monospace;
font-size: 0.8rem;
line-height: inherit;
margin: 0 2px;
max-width: inherit;
overflow: inherit;
padding: 2.2em 5px;
white-space: inherit;
}
.markdown-section code::after,
.markdown-section code::before {
letter-spacing: 0.05rem;
}
code .token {
-moz-osx-font-smoothing: initial;
-webkit-font-smoothing: initial;
min-height: 1.5rem;
position: relative;
left: auto;
}
pre::after {
color: #ccc;
content: attr(data-lang);
font-size: 0.6rem;
font-weight: 600;
height: 15px;
line-height: 15px;
padding: 5px 10px 0;
position: absolute;
right: 0;
text-align: right;
top: 0;
}
================================================
FILE: docs/.gitkeep
================================================
================================================
FILE: docs/adv-cpp/00.md
================================================
# 零、前言
## 大约
本节简要介绍作者、本书的内容、入门所需的技术技能,以及完成所有附带活动和练习所需的硬件和软件要求。
## 关于书
C++ 是使用最广泛的编程语言之一,应用于各种领域,从游戏到**图形用户界面** ( **GUI** )编程甚至操作系统。如果你想扩大你的职业机会,掌握 C++ 的高级特性是关键。
这本书从高级的 C++ 概念开始,帮助您破译复杂的 C++ 类型系统,并了解编译的各个阶段如何将源代码转换为目标代码。然后,您将学习如何识别需要用来控制执行流程、捕获数据和传递数据的工具。通过创建小模型,您甚至可以发现如何使用高级 lambdas,并在 C++ 中捕获和表达常见的 API 设计模式。在后面的章节中,您将通过学习内存对齐、缓存访问和程序运行时间来探索优化代码的方法。最后一章将帮助您通过理解现代的 CPU 分支预测以及如何使代码缓存友好来最大化性能。
到这本书的最后,你将会发展出与其他 C++ 程序员不同的编程技能。
### 关于作者
**Gazihan Alankus** 拥有圣路易斯华盛顿大学计算机科学博士学位。目前,他是土耳其伊兹密尔经济大学的助理教授。他教授并从事游戏开发、移动应用开发和人机交互方面的研究。他是谷歌 Dart 的开发专家,在他 2019 年创立的公司 Gbot 中与学生一起开发 Flutter 应用。
**Olena Lizina** 是一名拥有 5 年 C++ 经验的软件开发人员。她拥有为一家国际产品公司开发监控和管理远程计算机系统的实用知识。在过去的 4 年里,她一直在为国际外包公司的汽车项目工作,以解决众所周知的汽车问题。她一直参与不同项目的复杂和高性能应用的开发,如**人机界面**、导航和传感器应用。
**Rakesh Mane** 在软件行业拥有超过 18 年的经验。他曾与来自印度、美国和新加坡等不同地区的熟练程序员合作。他主要从事 C++、Python、shell 脚本和数据库方面的工作。在业余时间,他喜欢听音乐和旅行。此外,他喜欢使用软件工具和代码玩、试验和破坏东西。
**Vivek Nagarajan** 是一名自学成才的程序员,他从上世纪 80 年代开始研究 8 位系统。他从事过大量的软件项目,拥有 14 年的 C++ 专业经验。除此之外,他多年来一直致力于各种各样的语言和框架。他是一个业余力量爱好者,自己动手做的爱好者,也是摩托车赛车手。他目前是一名独立的软件顾问。
**Brian Price** 在各种语言、项目和行业拥有超过 30 年的工作经验,其中包括超过 20 年的 C++ 经验。他从事电站模拟器、SCADA 系统和医疗设备的工作。他目前正在用 C++、CMake 和 Python 为下一代医疗设备制作软件。他喜欢用各种语言解谜和欧拉项目。
### 学习目标
本书结束时,您将能够:
* 深入研究 C++ 的剖析和工作流程
* 研究 C++ 中不同编码方法的优缺点
* 测试、运行和调试您的程序
* 将对象文件链接为动态库
* 使用模板、SFINAE、constexpr if 表达式和变量模板
* 将最佳实践应用于资源管理
### 观众
如果你曾在 C++ 工作过,但想学习如何充分利用这种语言,尤其是对于大型项目,这本书是为你准备的。必须对编程有一个大致的了解,并且了解如何使用编辑器在项目目录中生成代码文件。也推荐一些强类型语言的经验,比如 C 和 C++。
### 进场
这本快节奏的书旨在通过描述性的图形和挑战性的练习,快速教你概念。这本书将有“号召”,有关键的要点和最常见的陷阱来保持你的兴趣,同时将主题分成易于管理的部分。
### 硬件要求
为了获得最佳的学生体验,我们推荐以下硬件配置:
* 任何带有 Windows、Linux 或 macOS 的入门级 PC/Mac 都足够了
* 处理器:双核或同等处理器
* 内存:4 GB 内存(首选 8 GB)
* 存储:35 GB 可用空间
### 软件需求
您还需要提前安装以下软件:
* 操作系统:Windows 7 SP1 32/64 位,Windows 8.1 32/64 位,或 Windows 10 32/64 位,Ubuntu 14.04 或更高版本,或 macOS Sierra 或更高版本
* 浏览器:谷歌 Chrome 还是 Mozilla 火狐
### 安装和设置
在开始阅读本书之前,您需要安装本书中使用的以下库。您将在这里找到安装这些的步骤。
**安装 CMake**
我们将使用 CMake 版本 3.12.1 或更高版本。我们有两种安装选择。
选项 1:
如果您使用的是 Ubuntu 18.10,可以使用以下命令全局安装 CMake:
```cpp
sudo apt install cmake
```
运行以下命令时:
```cpp
cmake –version
```
您应该会看到以下输出:
```cpp
cmake version 3.12.1
CMake suite maintained and supported by Kitware (kitware.com/cmake).
```
如果您在这里看到的版本低于 3.12.1(例如 3.10),您应该使用以下说明在本地安装 CMake。
备选方案 2:
如果您使用的是旧的 Linux 版本,您可能会得到低于 3.12.1 的 CMake 版本。然后,您需要在本地安装它。使用以下命令:
```cpp
wget \
https://github.com/Kitware/CMake/releases/download/v3.15.1/cmake-3.15.1-Linux-x86_64.sh
sh cmake-3.15.1-Linux-x86_64.sh
```
看到软件许可证后,输入 *y* 并按*进入*。当询问安装位置时,键入 *y* 并再次按回车键。这应该会将其安装到系统中的新文件夹中。
现在,我们将该文件夹添加到我们的路径中。键入以下内容。请注意,在本文档中,第一行有点太长,并且换行。您应该将其写成一行,如下所示:
```cpp
echo "export PATH=\"$HOME/cmake-3.15.1-Linux-x86_64/bin:$PATH\"" >> .bash_profile
source .profile
```
现在,当您键入以下内容时:
```cpp
cmake –version
```
您应该会看到以下输出:
```cpp
cmake version 3.15.1
CMake suite maintained and supported by Kitware (kitware.com/cmake).
```
3.15.1 是撰写本文档时的最新版本。由于它比 3.12.1 更新,这将满足我们的目的。
**安装 Git**
通过键入以下内容测试当前安装:
```cpp
git --version
```
您应该会看到如下一行:
```cpp
git version 2.17.1
```
如果改为看到下面一行,则需要安装`git`:
```cpp
command 'git' not found
```
以下是如何在 Ubuntu 中安装`git`:
```cpp
sudo apt install git
```
**安装 g++**
通过键入以下内容测试当前安装:
```cpp
g++ --version
```
您应该会看到如下输出:
```cpp
g++ (Ubuntu 7.4.0-1ubuntu1~18.04) 7.4.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
```
如果未安装,请键入以下代码进行安装:
```cpp
sudo apt install g++
```
**安装忍者**
通过键入以下内容测试当前安装:
```cpp
ninja --version
```
您应该会看到如下输出:
```cpp
1.8.2
```
如果未安装,请键入以下代码进行安装:
```cpp
sudo apt install ninja-build
```
**安装 Eclipse CDT 和 cmake4eclipse**
安装 Eclipse CDT 有多种方法。为了获得最新的稳定版本,我们将使用官方安装程序。去这个网站下载 Linux 安装程序:[https://www.eclipse.org/downloads/packages/installer](https://www.eclipse.org/downloads/packages/installer)。
按照那里的说明,为 C/C++ 开发者安装**Eclipse IDE**。安装完成后,运行 Eclipse 可执行文件。如果没有更改默认配置,在终端中键入以下命令将运行它:
```cpp
~/eclipse/cpp-2019-03/eclipse/eclipse
```
您将选择一个工作区文件夹,然后在 Eclipse 主窗口中会出现一个**欢迎**选项卡。
现在,我们将安装`cmake4eclipse`。一个简单的方法是去这个网站,把**安装**图标拖到 Eclipse 窗口:[https://github.com/15knots/cmake4eclipse#installation](https://github.com/15knots/cmake4eclipse#installation)。它会要求您重新启动 Eclipse,之后您就可以修改 CMake 项目来使用 Eclipse 了。
**安装谷歌测试**
我们会在系统中安装`GoogleTest`,系统也会安装其他依赖它的包。编写以下命令:
```cpp
sudo apt install libgtest-dev google-mock
```
该命令为`谷歌测试`安装包含文件和源文件。现在,我们需要构建我们安装的源文件来创建`谷歌测试`库。为此,请运行以下命令:
```cpp
cd /usr/src/gtest
sudo cmake CMakeLists.txt
sudo make
sudo cp *.a /usr/lib
```
### 安装代码包
将该类的代码包复制到`C:/Code`文件夹中。
### 附加资源
这本书的代码包也托管在 https://github.com/TrainingByPackt/Advanced-CPlusPlus 的 GitHub 上。
我们还有来自 https://github.com/PacktPublishing/丰富的书籍和视频目录的其他代码包。看看他们!
================================================
FILE: docs/adv-cpp/01.md
================================================
# 一、可移植的 C++ 软件剖析
## 学习目标
本章结束时,您将能够:
* 建立代码构建测试过程
* 描述编译的各个阶段
* 破译复杂的 C++ 类型系统
* 用单元测试配置项目
* 将源代码转换为目标代码
* 编写可读的代码并调试它
在本章中,我们将学习建立将在整本书中使用的代码构建测试模型,编写漂亮的代码,并执行单元测试。
## 简介
C++ 是最古老、最流行的语言之一,可以用来编写高效的代码。它既像 C 一样“接近金属”,又像 Java 一样具有高级的面向对象特性。作为一种高效的低级语言,C++ 成为游戏、模拟和嵌入式系统等效率至上的领域的首选语言。同时,作为一种具有泛型、引用和无数其他高级特性的面向对象语言,它适合由多人开发和维护的大型项目。
几乎任何编程经验都包括组织代码库和使用他人编写的库。C++ 也不例外。除非您的程序很简单,否则您会将代码分发到需要组织的多个文件中,并且您会使用各种库来完成任务,通常比您的代码更加高效和健壮。不使用任何第三方库的 C++ 项目是边缘案例,不代表使用许多库的大多数项目。这些项目及其库有望在不同的硬件架构和操作系统中工作。因此,如果要用 C++ 开发任何有意义的东西,花时间在项目设置上并理解用于管理依赖关系的工具是很重要的。
大多数现代和流行的高级语言都有标准工具来维护项目、构建项目以及处理它们的库依赖关系。其中许多都有存放库和工具的存储库,这些工具可以自动从这些存储库中下载和使用库。例如,Python 有`pip`,负责下载和使用程序员想要使用的库的适当版本。同样的,JavaScript 有`npm`,Java 有`maven`,Dart 有`pub`,C#有`NuGet`。在大多数语言中,您会列出库的名称和您想要使用的版本,工具会自动下载并使用库的兼容版本。这些语言受益于这样一个事实,即程序是在一个受控的环境中构建和运行的,在该环境中满足了一定级别的硬件和软件要求。另一方面,C++ 有望在各种不同架构的环境中工作,包括非常原始的硬件。因此,C++ 程序员在构建程序和执行依赖管理时不会那么娇纵。
## 管理 C++ 项目
在 C++ 世界中,我们有几个工具可以帮助管理项目源及其依赖关系。比如`pkg-config`、`自动工具`、`make`、`CMake`都是社区中最引人注目的。与其他高级语言的工具相比,这些工具的使用要复杂得多。`CMake`作为管理 C++ 项目及其依赖关系的事实标准已经在这些项目中兴起。它比`make`更固执己见,被大多数 IDEs(集成开发环境)接受为直接的项目格式。
虽然`CMake`有助于管理项目及其依赖关系,但这种体验仍然远远不是更高级的语言,在更高级的语言中,您可以列出您想要使用的库及其版本,其他一切都为您考虑。使用 CMake,您仍然有责任在您的开发环境中正确安装库,并且您应该为每个库使用兼容的版本。在具有大量包管理器的流行 Linux 发行版中,您可以轻松安装大多数流行库的二进制版本。但是,有时,您可能需要自己编译和安装库。这是整个 C++ 开发人员体验的一部分,您将通过更多地了解自己选择的开发平台来获得这一体验。在这里,我们将更加关注如何正确设置我们的 CMake 项目,包括理解和解决与库相关的问题。
### 代码构建测试运行循环
为了将我们的讨论建立在坚实的基础上,我们将立即从一个实际的例子开始。我们将从一个 C++ 代码基础模板开始,您可以将其用作自己项目的起点。我们将看到如何在命令行上使用 CMake 构建和编译它。我们还将为 C/C++ 开发人员设置 Eclipse IDE,并导入我们的 CMake 项目。集成开发环境的使用将为我们提供易于创建源代码的工具,并使我们能够一行行地调试程序,以查看程序执行过程中到底发生了什么,并以明智的方式纠正我们的错误,而不是反复试验和迷信。
### 打造一个 CMake 项目
C++ 项目事实上的标准是使用 CMake 来组织和构建项目。在这里,我们将使用一个基本的模板项目作为起点。以下是示例模板的文件夹结构:

###### 图 1.1:示例模板的文件夹结构
在上图中,**。gitignore** 文件列出了不应该添加到`git`版本控制系统的文件模式。这种被忽略的文件包括构建过程的输出,这些输出是在本地创建的,不应该在计算机之间共享。
**中的文件包括**和 **src** 文件夹是实际的 C++ 源文件, **CMakeLists.txt** 文件是通过处理**源代码编译规则**、**库依赖项**和其他项目设置将项目粘合在一起的 CMake 脚本文件。CMake 规则是独立于平台的高级规则。CMake 用它们创建各种类型的`为不同平台制作`文件。
用 CMake 构建一个项目是一个两步的过程。首先,我们让 CMake 为将编译和构建项目的本机构建系统生成平台相关的配置文件。然后,我们将使用生成的文件来构建项目。CMake 可以为其生成配置文件的平台相关构建系统包括 **UNIX** **Makefiles** 、 **Ninja** **构建文件**、 **NMake** **Makefiles** 、**MinGW****Makefiles**。这里的选择取决于使用的平台、这些工具的可用性以及个人偏好。**UNIX****Makefiles**是 **Unix** 和 **Linux** 的事实标准,而 **NMake** 是其 **Windows** 和 **Visual Studio** 的对应物。 **MinGW** 则是 **Windows** 中类似**的 Unix 环境,其中 **Makefiles** 也在使用。 **Ninja** 是一个现代化的构建系统,与其他构建系统相比,它提供了非凡的速度,加上多平台支持,我们选择在这里使用。此外,除了这些命令行构建系统之外,我们还可以为 **Visual Studio** 、 **XCode** 、 **Eclipse CDT** 和许多其他项目生成 ide 项目,并在 IDE 内部构建我们的项目。因此, **CMake** 是一个元工具,它将为实际构建项目的另一个系统创建配置文件。在下一节中,我们将解决一个练习,其中我们将使用 **CMake** 生成**忍者** **构建文件**。**
### 练习 1:使用 CMake 生成忍者构建文件
在本练习中,我们将使用`CMake`生成`忍者构建文件`,用于构建 C++ 项目。我们将首先从一个`git`存储库中下载我们的源代码,并将使用 CMake 和 Ninja 来构建它。本练习的目的是使用 CMake 生成 Ninja 构建文件,构建项目,然后运行它们。
#### 注意
GitHub 资源库的链接可以在这里找到:[https://GitHub . com/trainingypbackt/Advanced-CPlusPlus/tree/master/lesson 1/练习 01/project](https://github.com/TrainingByPackt/Advanced-CPlusPlus/tree/master/Lesson1/Exercise01/project) 。
执行以下步骤完成练习:
1. In a terminal window, type the following command to download the `CxxTemplate` repository from GitHub onto your local system:
```cpp
git clone https://github.com/TrainingByPackt/Advanced-CPlusPlus/tree/master/Lesson1/Exercise01/project
```
前一个命令的输出类似于以下内容:

###### 图 1.2:从 GitHub 签出示例项目
现在你在`CxxTemplate`文件夹中有了源代码。
2. 通过在终端中键入以下命令,导航到`CxxTemplate`文件夹:
```cpp
cd CxxTemplate
```
3. 现在,您可以通过键入以下命令列出项目中的所有文件:
```cpp
find .
```
4. Generate our Ninja build file using the `cmake` command in the `CxxTemplate` folder. To do that, write the following command:
```cpp
cmake -Bbuild -H. -GNinja
```
前面命令的输出如下:

###### 图 1.3:生成忍者构建文件
让我们解释前面命令的部分内容。通过`-Bbuild`,我们告诉 CMake 使用`构建`文件夹来生成构建工件。由于此文件夹不存在,CMake 将创建它。借助`–h .`,我们告诉 CMake 使用当前文件夹作为源。通过使用一个单独的`构建`文件夹,我们将保持源文件的干净,所有的构建工件将保存在`构建`文件夹中,由于我们的`,Git 忽略了这个文件夹。gitignore`文件。借助`–GNinja`,我们告诉 CMake 使用忍者构建系统。
5. Run the following commands to list the project files and to check the files that were created inside the `build` folder:
```cpp
ls
ls build
```
前面的命令将在终端中显示以下输出:

###### 图 1.4:构建文件夹中的文件
很明显,前面的文件将出现在构建文件夹中。 **build.ninja** 和 **rules.ninja** 在前面的输出中是 Ninja build 文件,可以在这个平台中实际构建我们的项目。
#### 注意
通过使用 CMake,我们不需要编写忍者构建文件,并且避免了提交到 Unix 平台。相反,我们有一个元构建系统,可以为其他平台(如 UNIX/Linux、MinGW 和 Nmake)生成低级构建文件。
6. Now, go into the `build` folder and build our project by typing the following commands in the terminal:
```cpp
cd build
ninja
```
您应该会看到如下所示的最终输出:

###### 图 1.5:用忍者建造
7. Type `ls` in the **build** folder and check whether we have generated the `CxxTemplate` executable or not:
```cpp
ls
```
前面的命令在终端中产生以下输出:

###### 图 1.6:运行 ninja 后构建文件夹中的文件
在上图中,可以看到生成了`CxxTemplate`可执行文件。
8. In the terminal, type the following command to run the `CxxTemplate` executable:
```cpp
./CxxTemplate
```
终端中的前一个命令将提供以下输出:

###### 图 1.7:运行可执行文件
`src/CxxTemplate.cpp`文件中的下面一行负责写入前面的输出:
```cpp
std::cout << "Hello CMake." << std::endl;
```
现在你已经在 Linux 中成功构建了一个 CMake 项目。忍者和 CMake 配合得相当好。你只需要运行一次 CMake,Ninja 会检测是否应该再次调用 CMake,并为你调用它。例如,即使你在你的`CMakeLists.txt`文件中添加了新的源文件,你只需要在终端中键入`忍者`命令,它就会自动运行 CMake 为你更新忍者构建文件。既然您已经了解了在 Linux 中构建 CMake 项目,在下一节中,我们将了解如何将 CMake 项目导入到 Eclipse CDT 中。
## 将一个项目导入 Eclipse CDT
忍者构建文件对于在 Linux 中构建我们的项目非常有用。然而,一个 CMake 项目是可移植的,也可以用于其他构建系统和 ide。许多 ide 接受 CMake 作为它们的配置文件,并在您修改和构建项目时提供无缝的体验。在本节中,我们将讨论如何将一个 CMake 项目导入到 Eclipse CDT 中,这是一个流行的跨平台 C/C++ IDE。
有多种方法可以将 Eclipse CDT 与 CMake 一起使用。CMake 提供的默认选项是 IDE 项目的单向生成。在这里,您只需创建一次集成开发环境项目,对集成开发环境项目所做的任何修改都不会变回原始的 CMake 项目。如果您将项目作为一个 CMake 项目来管理,并使用 Eclipse CDT 进行一次性构建,这将非常有用。然而,如果您想在 Eclipse CDT 中进行开发,这并不理想。
在 Eclipse CDT 中使用 CMake 的另一种方法是使用自定义的`cmake4eclipse`插件。使用这个插件的时候,不要放弃你的`CMakeLists.txt`文件,单向切换到 Eclipse CDT 自己的项目经理。相反,您继续通过`CMakeLists.txt`文件来管理您的项目,该文件仍然是您项目的主要配置文件。Eclipse CDT 积极地与您的`CMakeLists.txt`文件合作来构建您的项目。您可以在您的`CMakeLists.txt`中添加或删除源文件并进行其他更改,并且`cmake4eclipse`插件会在每次构建时将这些更改应用到 Eclipse CDT 项目中。您将有一个很好的集成开发环境体验,同时保持您的 CMake 项目最新。这种方法的好处是,您可以随时退出使用 Eclipse CDT,稍后使用您的`CMakeLists.txt`文件切换到另一个构建系统(如 Ninja)。我们将在下面的练习中使用第二种方法。
### 练习 T1】E2:将 CMake 文件导入 Eclipse CDT
在上一个练习中,您开发了一个 CMake 项目,并且希望开始使用 Eclipse CDT IDE 来编辑和构建该项目。在本练习中,我们将使用`cmake4eclipse`插件将我们的 CMake 项目导入到 Eclipse CDT IDE 中。执行以下步骤完成练习:
1. 打开 Eclipse CDT。
2. Create a new C++ project in the location of our current project (the folder that contains the `CMakeLists.txt` file and the **src** folder). Go to **File** | **New** | **Project**. A **New Project** dialog box appears like the one in the following screenshot:

###### 图 1.8:新建项目对话框
3. Select the **C++ Project** option and click on the **Next** button. A **C++ Project** dialog box appears like the one in the following screenshot:

###### 图 1.9: C++ 项目对话框
4. 接受一切,包括切换到 C/C++ 视角,点击**完成**。
5. Click on the **Restore** button at the top-left corner to view the newly created project:

###### 图 1.10:恢复按钮
6. Click on the **CxxTemplate** project. Go to **Project** | **Properties**, then select **Tool Chain Editor** under **C/C++ Build** from the left pane and set **Current builder** to **CMake Builder (portable)**. Then, click on the **Apply and Close** button:

###### 图 1.11:项目属性
7. Then, choose the **Project** | **Build All** menu item to build the project:

###### 图 1.12:构建项目
8. In the following **Console** pane, you will see the output of CMake as if you called it from the command line, followed by a call to `make all` that actually builds our project:

###### 图 1.13:构建输出
9. If you did not get any errors in the previous steps, you can run the project using the menu item **Run** | **Run**. If you are given some options, choose **Local C/C++ Application** and **CxxTemplate** as the executable:

###### 图 1.14:运行项目
10. 当它运行时,您将在**控制台**窗格中看到程序的输出,如下所示:

###### 图 1.15:项目输出
您已经使用 Eclipse CDT 成功地构建并运行了一个 CMake 项目。在下一个练习中,我们将通过添加新的源文件和新的类来频繁地改变我们的项目。
### 练习 3:向 CMake 和 Eclipse CDT 添加新的源文件
当您开发大得多的 C++ 项目时,您将倾向于随着项目的增长向其中添加新的源文件,以满足设定的期望。在本练习中,我们将添加一个新的`。cpp`和`。h`文件对到我们的项目,看看 CMake 和 Eclipse CDT 是如何配合这些变化一起工作的。我们将使用新建类向导在项目中添加这些文件,但是您也可以使用任何其他文本编辑器创建它们。执行以下步骤向 CMake 和 Eclipse CDT 添加新的源文件:
1. First, open the project that we have been using until now. In the **Project Explorer** pane on the left, expand the root entry, **CxxTemplate**, and you will see the files and folders of our project. Right-click the **src** folder and select **New** | **Class** from the pop-up menu:

###### 图 1.16:创建一个新类
2. 在打开的对话框中,为类名键入**一个类**。点击**完成**按钮,会看到 **ANewClass.cpp** 和 **ANewClass.h** 文件生成在 **src** 文件夹下。
3. Now, let's write some code into the `ANewClass` class and access it from the **CxxTemplate** class that we already had. Open `ANewClass.cpp` and change the beginning of the file to match the following, and then save the file:
```cpp
#include "ANewClass.h"
#include
void ANewClass::run() {
std::cout << "Hello from ANewClass." << std::endl;
}
```
您将会看到 Eclipse 通过一条**未找到成员声明**消息来警告我们:

###### 图 1.17:分析仪警告
产生这个错误是因为我们需要将它添加到我们的`ANewClass.h`文件中。这样的警告可以通过 IDEs 中的分析器来实现,并且非常有用,因为它们可以帮助您在键入时修复代码,而无需运行编译器。
4. Open the `ANewClass.h` file, add the following code, and save the file:
```cpp
public:
void run(); // we added this line
ANewClass();
```
你应该看到`中的错误。cpp`文件走了。如果它没有消失,可能是因为您可能忘记保存其中一个文件。你应该养成按 *Ctrl + S* 保存当前文件的习惯,或者按 *Shift + Ctrl + S* 保存所有你编辑过的文件。
5. Now, let's use this class from our other class, `CxxTemplate.cpp`. Open that file, perform the following modifications, and save the file. Here, we are first importing header files and in the constructor of `CxxApplication`, we are printing text to the console. Then, we are creating a new instance of `ANewClass` and calling its `run` method:
```cpp
#include "CxxTemplate.h"
#include "ANewClass.h"
#include
...
CxxApplication::CxxApplication( int argc, char *argv[] ) {
std::cout << "Hello CMake." << std::endl;
::ANewClass anew;
anew.run();
}
```
#### 注意
这个文件的完整代码可以在这里找到:[https://github . com/trainingypbackt/Advanced-CPlusPlus/blob/master/lesson 1/execute 03/src/cxxtemplate . CPP](https://github.com/TrainingByPackt/Advanced-CPlusPlus/blob/master/Lesson1/Exercise03/src/CxxTemplate.cpp)。
6. Try to build the project by clicking on the **Project** | **Build All** menu options. You will get some undefined reference errors in two lines. This is because our project is built with CMake's rules and we did not let CMake know about this new file. Open the `CMakeLists.txt` file, make the following modification, and save the file:
```cpp
add_executable(CxxTemplate
src/CxxTemplate.cpp
src/ANewClass.cpp
)
```
尝试再次构建项目。这次你应该看不到任何错误。
7. 使用**运行** | **运行**菜单选项运行项目。您应该会在终端中看到以下输出:

###### 图 1.18:程序输出
您修改了一个 CMake 项目,向其中添加了新文件,并且运行良好。请注意,我们在`src`文件夹中创建了文件,并让`CMakeLists.txt`文件知道了 CPP 文件。如果您不使用 Eclipse,您可以简单地继续使用常见的 CMake 构建命令,您的程序将成功运行。到目前为止,我们已经检查了来自 GitHub 的示例代码,并用普通的 CMake 和 Eclipse IDE 构建了它。我们还在 CMake 项目中添加了一个新的类,并在 Eclipse IDE 中重新构建了它。现在您知道如何构建和修改 CMake 项目了。在下一节中,我们将执行一个向项目中添加新的源文件-头文件对的活动。
### 活动 1:向项目添加一个新的源文件-头文件对
在开发 C++ 项目时,随着项目的增长,您会向其中添加新的源文件。出于各种原因,您可能希望添加新的源文件。例如,假设您正在开发一个会计应用,在该应用中,您在项目的许多地方计算利率,并且您希望在单独的文件中创建一个函数,以便在整个项目中重用它。为了简单起见,这里我们将创建一个简单的求和函数。在本练习中,我们将向项目中添加一个新的源文件-头文件对。执行以下步骤完成活动:
1. 在 Eclipse IDE 中打开我们在前面的练习中创建的项目。
2. 将`SumFunc.cpp`和`SumFunc.h`文件对添加到项目中。
3. 创建一个名为`sum`的简单函数,返回两个整数的和。
4. 从`CxxTemplate`类构造函数调用该函数。
5. 在 Eclipse 中构建和运行项目。
预期输出应该类似于以下内容:

###### 图 1.19:最终输出
#### 注意
这项活动的解决方案可以在第 620 页找到。
在下一节中,我们将讨论如何为我们的项目编写单元测试。通常将项目分成许多类和函数,它们一起工作来实现期望的目标。您必须用单元测试来管理这些类和函数的行为,以确保它们以预期的方式运行。
## 单元测试
单元测试通常是编程的重要部分。基本上,单元测试是一些小程序,它们在各种场景中使用我们的类并产生预期的结果,在我们的项目中以并行的文件层次结构存在,最终不会出现在实际的可执行文件中,而是由我们在开发过程中单独执行,以确保我们的代码以预期的方式运行。我们应该为我们的 C++ 程序编写单元测试,以确保它们在每次更改后都像预期的那样运行。
### 准备 U nit 测试
有几个 C++ 测试框架我们可以和 CMake 一起使用。我们将使用**谷歌测试**,它比其他选项有几个好处。在下一个练习 e 中,我们将使用谷歌测试为单元测试准备我们的项目。
### 练习 4:准备我们的单元测试项目
我们已经安装了谷歌测试,但我们的项目没有设置为使用谷歌测试进行单元测试。除了安装,还有一些设置需要在我们的 CMake 项目中进行,以便进行谷歌测试单元测试。按照以下步骤实施本练习:
1. 打开 Eclipse CDT,选择我们一直在使用的 CxxTemplate 项目。
2. 创建一个名为**测试**的新文件夹,因为我们将在那里执行所有测试。
3. Edit our base `CMakeLists.txt` file to allow tests in the **tests** folder. Note that we already had code to find the `GTest` package that brings `GoogleTest` capability to CMake. We will add our new lines just after that:
```cpp
find_package(GTest)
if(GTEST_FOUND)
set(Gtest_FOUND TRUE)
endif()
if(GTest_FOUND)
include(GoogleTest)
endif()
# add these two lines below
enable_testing()
add_subdirectory(tests)
```
这就是我们需要添加到主`CMakeLists.txt`文件中的全部内容。
4. 在我们的**测试**文件夹中创建另一个**文件。这将被使用,因为我们在主 **CMakeLists.txt** 文件中有**add _ 子目录(测试)**行。这个**测试/CMakeLists.txt** 文件将管理测试源。**
5. Add the following code in the `tests/CMakeLists.txt` file:
```cpp
include(GoogleTest)
add_executable(tests CanTest.cpp)
target_link_libraries(tests GTest::GTest)
gtest_discover_tests(tests)
```
让我们一行行地剖析这段代码。第一行引入了谷歌测试功能。第二行创建**测试**可执行文件,它将包括我们所有的测试源文件。在这种情况下,我们只有一个 **CanTest.cpp** 文件,它将只是验证测试工作。之后,我们将 **GTest** 库链接到**测试**可执行文件。最后一行标识了可执行的**测试**中的所有单独测试,并将它们添加到**中作为测试。这样,各种测试工具将能够告诉我们哪些单独的测试失败了,哪些通过了。**
6. 创建一个`测试/CanTest.cpp`文件。添加这段代码只是为了验证测试正在运行,而不是实际测试我们实际项目中的任何东西:
```cpp
#include "gtest/gtest.h"
namespace {
class CanTest: public ::testing::Test {};
TEST_F(CanTest, CanReallyTest) {
EXPECT_EQ(0, 0);
}
}
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
```
`TEST_F`线为单独测试。现在,`EXPECT_EQ(0,0)`正在测试零是否等于零,如果我们真的能运行测试,总是会成功的。稍后,我们将在这里添加我们自己的类的结果,以针对各种值进行测试。现在,我们已经在我们的项目中为谷歌测试进行了必要的设置。接下来,我们将构建并运行这些测试。
### 构建、运行、和编写单元测试
现在,我们将讨论如何构建、运行和编写单元测试。到目前为止,我们的例子是一个简单的虚拟测试,已经准备好构建和运行。稍后,我们将添加更有意义的测试,并查看通过和失败测试的输出。在下面的练习中,我们将为我们在前面练习中创建的项目构建、运行和编写单元测试。
### 练习 5:构建 g 并运行测试
到目前为止,您已经创建了一个设置了`GoogleTest`的项目,但是您没有构建或运行我们创建的测试。在本练习中,我们将构建并运行我们创建的测试。由于我们使用`add _ 子目录`添加了我们的`测试`文件夹,构建项目将自动构建测试。运行测试需要更多的努力。执行以下步骤完成练习:
1. 在 Eclipse CDT 中打开我们的 CMake 项目。
2. To build the tests, simply build the project just like you did before. Here is the output of building the project one more time from Eclipse after a full build using **Project** | **Build All**:

###### 图 1.20:构建操作及其输出
3. If you do not see this output, your console may be in the wrong view. You can correct it as shown in the following figures:

###### 图 1.21:查看正确的控制台输出

###### 图 1.22:查看正确的控制台输出
如您所见,我们的项目现在有两个可执行的目标。他们都生活在`构建`文件夹中,就像任何其他构建神器一样。它们的位置是`构建/调试/扩展`和`构建/调试/测试/测试`。因为它们是可执行文件,所以我们可以简单地运行它们。
4. We ran `CxxTemplate` before and will not see any extra output now. Run the other executable by typing the following command in the terminal while we are in our project folder:
```cpp
./build/Debug/tests/tests
```
上述代码在终端中生成以下输出:

###### 图 1.23:运行可执行的测试
这是我们的`测试`可执行文件的简单输出。如果你想看看测试是否通过,你可以简单地运行这个。然而,测试远不止于此。
5. One of the ways you can run your tests is by using the `ctest` command. Write the following commands in the terminal while you are in the project folder. We go to the folder where the `tests` executable resides, run `ctest` there, and come back:
```cpp
cd build/Debug/tests
ctest
cd ../../..
```
这是您将看到的输出:

###### 图 1.24:运行 ctest
#### 注意
`ctest`命令可以运行您的`测试`可执行文件,其中有许多选项,包括自动将测试结果提交到在线仪表板的能力。在这里,我们将简单地运行`ctest`命令;它的其他特性留给感兴趣的读者作为练习。您可以键入`ctest - help`或访问在线文档,在[https://cmake.org/cmake/help/latest/manual/ctest.1.html#](https://cmake.org/cmake/help/latest/manual/ctest.1.html#)进一步了解`ctest`。
6. 运行测试的另一种方法是在 Eclipse 中以一种漂亮的图形报告格式运行它们。为此,我们将创建一个测试感知的运行配置。在 Eclipse 中,点击**运行** | **运行配置…** ,右键点击左侧 **C/C++ 单元**,选择**新配置**。
7. Change the name from **CxxTemplate Debug** to **CxxTemplate Tests** as follows:

###### 图 1.25:更改运行配置的名称
8. Under **C/C++ Application**, select the **Search Project** option:

###### 图 1.26:运行配置
9. Choose **tests** in the new dialog:

###### 图 1.27:创建测试运行配置并选择可执行的测试
10. Next, go to the **C/C++ Testing** tab and select **Google Tests Runner** in the dropdown. Click on **Apply** at the bottom of the dialog and click on the **Run** option for the test that we have to run for the first time:

###### 图 1.28:运行配置
11. 在接下来的运行中,您可以点击工具栏中播放按钮旁边的下拉菜单,或者选择**运行** | **运行历史**选择**扩展测试**:

###### 图 1.29:最终确定运行配置设置并选择要运行的配置
结果将类似于下面的截图:

###### 图 1.30:运行单元测试的结果
这是一份很好的报告,包含了所有测试的条目——目前只有一个。如果不想离开集成开发环境,您可能更喜欢这样。此外,当您有许多测试时,这个界面可以帮助您有效地过滤它们。现在,您已经构建并运行了使用谷歌测试编写的测试。您以几种不同的方式运行它们,包括直接执行测试、使用`ctest`和使用 Eclipse CDT。在下一节中,我们将解决一个练习,其中我们将实际测试代码的功能。
### 练习 6:测试代码的功能
您已经运行了简单的测试,但是现在您想要编写测试功能的有意义的测试。在最初的活动中,我们创建了`SumFunc.cpp`,它具有`sum`功能。现在,在本练习中,我们将为该文件编写一个测试。在本测试中,我们将使用`求和`功能将两个数字相加,并验证结果是否正确。让我们用之前的`sum`函数回忆一下以下文件的内容:
* `src/SumFunc.h` :
```cpp
#ifndef SRC_SUMFUNC_H_
#define SRC_SUMFUNC_H_
int sum(int a, int b);
#endif /* SRC_SUMFUNC_H_ */
```
* `src/SumFunc.cpp` :
```cpp
#include "SumFunc.h"
#include
int sum(int a, int b) {
return a + b;
}
```
* `CMakeLists.txt`相关行:
```cpp
add_executable(CxxTemplate
src/CxxTemplate.cpp
src/ANewClass.cpp
src/SumFunc.cpp
)
```
另外,让我们回忆一下我们的`cantest . CPP`文件,它有我们单元测试的`main()`功能:
```cpp
#include "gtest/gtest.h"
namespace {
class CanTest: public ::testing::Test {};
TEST_F(CanTest, CanReallyTest) {
EXPECT_EQ(0, 0);
}
}
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
```
执行以下步骤完成练习:
1. 在 Eclipse CDT 中打开我们的 CMake 项目。
2. Add a new test source file (`tests/SumFuncTest.cpp`) with the following content:
```cpp
#include "gtest/gtest.h"
#include "../src/SumFunc.h"
namespace {
class SumFuncTest: public ::testing::Test {};
TEST_F(SumFuncTest, CanSumCorrectly) {
EXPECT_EQ(7, sum(3, 4));
}
}
```
请注意,这没有`main()`功能,因为`CanTest.cpp`有一个功能,这些功能将链接在一起。其次,注意这包括`SumFunc.h`,它在项目的 **src** 文件夹中,在测试中用作`sum(3,4)`。这就是我们在测试中使用项目代码的方式。
3. Make the following change in the `tests/CMakeLists.txt` file to build the test:
```cpp
include(GoogleTest)
add_executable(tests CanTest.cpp SumFuncTest.cpp ../src/SumFunc.cpp) # added files here
target_link_libraries(tests GTest::GTest)
gtest_discover_tests(tests)
```
注意,我们添加了测试(`SumFuncTest.cpp`)和它测试的代码(`../src/SumFunc.cpp`)转换为可执行文件,因为我们的测试代码使用的是实际项目中的代码。
4. Build the project and run the test as before. You should see the following report:

###### 图 1.31:运行测试后的输出
我们可以将这样的测试添加到我们的项目中,所有的测试都会出现在屏幕上,如前面的截图所示。
5. Now, let's add one more test that will actually fail. In the `tests/SumFuncTest.cpp` file, make the following change:
```cpp
TEST_F(SumFuncTest, CanSumCorrectly) {
EXPECT_EQ(7, sum(3, 4));
}
// add this test
TEST_F(SumFuncTest, CanSumAbsoluteValues) {
EXPECT_EQ(6, sum(3, -3));
}
```
请注意,该测试假设输入的绝对值相加,这是不正确的。这个调用的结果是`0`,但是在这个例子中预计是`6`。这是我们在项目中添加这个测试所必须做的唯一改变。
6. Now, build the project and run the test. You should see this report:

###### 图 1.32:构建报告
从上图中可以看到,前两次测试通过,最后一次测试失败。当我们看到这个输出时,有两种选择:要么我们的项目代码是错误的,要么测试是错误的。在这种情况下,我们的测试是错误的。这是因为我们的 **CanSumAbsoluteValues** 测试用例期望`6`等于`sum(3,-3)`。这是因为我们假设我们的函数对提供的整数的绝对值求和。然而,事实并非如此。我们的函数只是将给定的数字相加,不管它们是正数还是负数。因此,这个测试有一个错误的假设,失败了。
7. 让我们更改测试并修复它。更改测试,使我们预期`-3`和`3`之和为`0`。重命名测试,以反映该测试的实际作用:
```cpp
TEST_F(SumFuncTest, CanSumCorrectly) {
EXPECT_EQ(7, sum(3, 4));
}
// change this part
TEST_F(SumFuncTest, CanUseNegativeValues) {
EXPECT_EQ(0, sum(3, -3));
}
```
8. 现在运行它,并在报告中观察所有测试是否通过:

###### 图 1.33:测试执行成功
最后,我们已经在我们的系统和项目中使用 CMake 建立了谷歌测试。我们还使用谷歌测试在终端和 Eclipse 中编写、构建和运行单元测试。理想情况下,您应该为每个类编写单元测试,并涵盖所有可能的用法。您还应该在每次重大更改后运行测试,并确保不破坏现有代码。在下一节中,我们将执行形成一个添加新类及其测试的活动。
### 活动 2:在测试中添加一个新类
当您开发一个 C++ 项目时,您会随着项目的增长向其中添加新的源文件。您还为他们编写测试,以确保他们正常工作。在本练习中,我们将添加一个模拟`1D`直线运动的新类。该类将具有用于`位置`和`速度`的双字段。它还将有一个`advanceTimeBy()`方法,该方法接收一个双`dt`参数,该参数基于`速度`的值修改`位置`。双数值用`EXPECT_DOUBLE_EQ`代替`EXPECT_EQ`。在本活动中,我们将向项目中添加一个新类及其测试。按照以下步骤执行本活动:
1. 打开我们在 Eclipse 集成开发环境中创建的项目。
2. 将`LinearMotion1D.cpp`和`LinearMotion1D.h`文件对添加到包含`LinearMotion1D`类的项目中。在这个类中,创建两个双字段:`位置`和`速度`。另外,创建一个`提前时间比(双 dt)`功能,修改`位置`。
3. 在`测试/linear motion 1 test . CPP`文件中为此编写测试。写两个代表两个不同方向运动的测试。
4. 在 Eclipse IDE 中构建并运行它。
5. 验证测试是否通过。
最终测试结果应该类似于以下内容:

###### 图 1.34:最终测试结果
#### 注意
这项活动的解决方案可以在第 622 页找到。
添加新类及其测试是 C++ 开发中非常常见的任务。我们创建类有各种原因。有时,我们有一个很好的软件设计计划,我们创建它所需要的类。其他时候,当一个类变得过于庞大和单一时,我们会以一种有意义的方式将它的一些职责分离给另一个类。让这个任务变得实际很重要,这样可以防止你拖拖拉拉,最终得到一个巨大的整体类。在下一节中,我们将讨论编译和链接阶段会发生什么。这将让我们更好地了解 C++ 程序下正在发生的事情。
## 了解编译、链接和目标文件内容
使用 C++ 的一个主要原因是效率。C++ 让我们可以控制内存管理,这就是为什么理解对象在内存中的布局很重要。此外,C++ 源文件和库被编译成目标硬件的目标文件并链接在一起。通常,C++ 程序员必须处理链接器问题,这就是为什么理解编译步骤并能够研究目标文件很重要。另一方面,大型项目是由团队长时间开发和维护的,这就是为什么创建干净和可理解的代码很重要。与任何其他软件一样,C++ 项目中会出现错误,需要通过观察程序行为来仔细识别、分析和解决。因此,学习如何调试 C++ 代码也很重要。在下一节中,我们将学习如何创建高效的、与其他代码配合良好的、可维护的代码。
### 编译和链接步骤
C++ 项目是作为一组源代码文件和项目配置文件创建的,这些文件组织了源代码和库依赖项。在编译步骤中,首先将这些源转换为目标文件。在链接步骤中,这些目标文件被链接在一起形成可执行文件,这是项目的最终输出。项目使用的库也在这一步链接。
在接下来的练习中,我们将使用我们现有的项目来观察编译和链接阶段。然后,我们将手动重新创建它们,以更详细地查看流程。
### 练习 7:识别构建步骤
您一直在构建项目,而没有调查构建操作的细节。在本练习中,我们将研究项目构建步骤的细节。执行以下操作来完成练习:
1. 打开终端。
2. 通过键入以下命令导航到`构建`文件夹,我们的`Makefile`文件位于该文件夹中:
```cpp
cd build/Debug
```
3. Clean the project and run the build in `VERBOSE` mode using the following command:
```cpp
make clean
make VERBOSE=1 all
```
您将在终端中获得构建过程的详细输出,这可能看起来有点拥挤:

###### 图 1.35:构建过程第 1 部分

###### 图 1.36:构建过程第 2 部分

###### 图 1.37:完整的构建输出
下面是这个输出中的一些行。以下几行是与主可执行文件的编译和链接相关的重要内容:
```cpp
/usr/bin/c++ -g -pthread -std=gnu++ 1z -o CMakeFiles/CxxTemplate.dir/src/CxxTemplate.cpp.o -c /home/username/Packt/Cpp2019/CxxTemplate/src/CxxTemplate.cpp
/usr/bin/c++ -g -pthread -std=gnu++ 1z -o CMakeFiles/CxxTemplate.dir/src/ANewClass.cpp.o -c /home/username/Packt/Cpp2019/CxxTemplate/src/ANewClass.cpp
/usr/bin/c++ -g -pthread -std=gnu++ 1z -o CMakeFiles/CxxTemplate.dir/src/SumFunc.cpp.o -c /home/username/Packt/Cpp2019/CxxTemplate/src/SumFunc.cpp
/usr/bin/c++ -g -pthread -std=gnu++ 1z -o CMakeFiles/CxxTemplate.dir/src/LinearMotion1D.cpp.o -c /home/username/Packt/Cpp2019/CxxTemplate/src/LinearMotion1D.cpp
/usr/bin/c++ -g CMakeFiles/CxxTemplate.dir/src/CxxTemplate.cpp.o CMakeFiles/CxxTemplate.dir/src/ANewClass.cpp.o CMakeFiles/CxxTemplate.dir/src/SumFunc.cpp.o CMakeFiles/CxxTemplate.dir/src/LinearMotion1D.cpp.o -o CxxTemplate -pthread
```
4. The `c++ ` command here is just a symbolic link to the `g++ ` compiler. To see that it's actually a chain of symbolic links, type the following command:
```cpp
namei /usr/bin/c++
```
您将看到以下输出:

###### 图 1.38:用于/usr/bin/c++ 的符号链接链
因此,我们将在整个讨论中交替使用`c++ `和`g++ `。在我们前面引用的构建输出中,前四行是编译每个`。cpp`源文件并创建相应的`。o`对象文件。最后一行是将这些目标文件链接在一起,创建`CxxTemplate`可执行文件。下图直观地展示了这一过程:

###### 图 1.39:c++ 项目的执行阶段
如上图所示,作为目标的一部分添加到 CMake 的 CPP 文件,连同它们包含的头文件一起,被编译成目标文件,这些文件随后被链接在一起以创建目标可执行文件。
5. 为了进一步理解这个过程,让我们自己执行编译步骤。在终端中,转到项目文件夹,使用以下命令创建一个名为`mybuild`的新文件夹:
```cpp
cd ~/CxxTemplate
mkdir mybuild
```
6. 然后,运行以下命令将 CPP 源文件编译为目标文件:
```cpp
/usr/bin/c++ src/CxxTemplate.cpp -o mybuild/CxxTemplate.o -c
/usr/bin/c++ src/ANewClass.cpp -o mybuild/ANewClass.o -c
/usr/bin/c++ src/SumFunc.cpp -o mybuild/SumFunc.o -c
/usr/bin/c++ src/LinearMotion1D.cpp -o mybuild/LinearMotion1D.o -c
```
7. Go into the `mybuild` directory and see what's there using the following command:
```cpp
cd mybuild
ls
```
我们看到如下预期的输出。这些是我们的目标文件:

###### 图 1.40:编译的目标文件
8. 下一步,将目标文件链接在一起,形成我们的可执行文件。键入以下命令:
```cpp
/usr/bin/c++ CxxTemplate.o ANewClass.o SumFunc.o LinearMotion1D.o -o CxxTemplate
```
9. Now, let's see our executable among the list of files here by typing the following command:
```cpp
ls
```
这将在下图中显示新的`CxxTemplate`文件:

###### 图 1.41:链接的可执行文件
10. Now, run our executable by typing the following command:
```cpp
./CxxTemplate
```
看看我们之前的输出:

###### 图 1.42:可执行文件输出
现在,您已经检查了构建过程的细节并自己重新创建了它们,在下一节中,让我们来探索链接过程。
### 连接步骤
在这一节中,让我们来看看两个源文件之间的连接,以及它们如何在同一个可执行文件中结束。请看下图中的**求和**功能:

###### 图 1.43:链接过程
**sum** 函数的主体在 **SumFunc.cpp** 中定义。它在 **SumFunc.h** 中有一个远期申报。这样,想要使用 **sum** 函数的源文件就可以知道它的签名了。一旦他们知道它的签名,他们就可以调用它,并相信实际的函数定义将在运行时存在,而实际上与定义函数的 **SumFunc.cpp** 没有任何交互。
编译后,调用 **sum** 函数的 **CxxTemplate.cpp** 将该调用携带到其目标文件中。但是,它不知道函数定义在哪里。 **SumFunc.cpp** 的对象文件有这个定义,但是和 **CxxTemplate.o** 还没有关系。
在链接步骤中,链接器将 **CxxTemplate.o** 中的调用与 **SumFunc.o** 中的定义进行匹配。因此,该调用在可执行文件中运行良好。如果链接器没有找到 **sum** 函数的定义,它会给出一个链接器错误。
链接器通过名称和参数找到**和**函数。这叫做**解析符号**。对象文件中定义的类、函数和变量放在符号表中,对符号的每个引用都通过在该表中查找来解析。当符号不存在时,您会收到一个`符号无法解析`错误。
这让我们经历了构建过程的两个阶段:`编译`和`链接`。请注意,当我们手动编译源代码时,我们使用了比 CMake 更简单的命令。请随意输入`man g++ `查看所有选项。后来,我们讨论了链接和如何解决符号。我们还讨论了链接步骤中可能出现的问题。在下一节中,我们将了解目标文件。
### 深入:查看对象文件
为了使链接步骤没有错误,我们需要让所有的符号引用与我们的符号定义相匹配。大多数时候,我们可以通过查看源文件来分析事情将如何解决。有时,在复杂的情况下,我们可能很难理解为什么一个符号没有被解析。在这种情况下,查看对象文件的内容来研究引用和定义对于解决问题可能很有用。除了链接器错误之外,理解目标文件内容以及链接一般是如何工作的对 C++ 程序员来说也很有用。了解幕后发生的事情可能有助于程序员更好地理解整个过程。
当我们的源代码被编译成目标文件时,我们的语句和表达式被转换成汇编代码,这是中央处理器理解的低级语言。汇编中的每条指令都包含一个操作,后面是操作符,它们是中央处理器的寄存器。有向寄存器加载数据和从寄存器加载数据以及对寄存器中的值进行操作的指令。Linux 中的`objdump`命令帮助我们查看这些目标文件的内容。
#### 注意
我们将利用编译器资源管理器,这是一个很好的在线工具,更容易使用,你可以在左边的窗口写代码,在右边,你可以看到编译后的汇编代码。这是编译器浏览器的链接:https://godbolt.org。
### 练习 8:探索编译代码
在本练习中,我们将使用编译器资源管理器来编译一些简单的 C++ 代码,在这些代码中我们定义并调用一个函数。我们将研究编译后的程序集代码,以了解如何准确解析名称和进行调用。这将使我们更好地理解幕后发生了什么,以及我们的代码是如何以可执行格式工作的。执行以下步骤完成练习:
1. Add the following code in **Compiler Explorer**:
```cpp
int sum(int a, int b) {
return a + b;
}
int callSum() {
return sum(4, 5);
}
```
我们有两个功能;一个在呼叫另一个。下面是编译后的输出:

###### 图 1.44:编译后的代码
虽然不太清楚,但你或多或少能看出它在做什么。我们不打算深入讨论汇编代码的细节,但我们将重点关注在链接器阶段如何解析符号。现在让我们关注以下几行:
```cpp
sum(int, int):
...
callSum():
...
call sum(int, int)
...
```
`调用 sum(int,int)`行实现了您所期望的:它调用前面的`sum`函数,并将参数放在一些寄存器中。这里重要的一点是,函数是由它们的名称和参数类型按顺序标识的。链接器用这个签名寻找合适的函数。请注意,返回值不是签名的一部分。
2. Disable the **Demangle** checkbox and see how these function names are actually stored:

###### 图 1.45:没有解混的编译代码
这里,我们的台词变成了这样:
```cpp
_Z3sumii:
...
_Z7callSumv:
...
call _Z3sumii
...
```
前面是这些函数的错误名称。在`_Z`之后,数字告诉我们函数名有多长,以便正确解释下面的字母。在函数名之后,没有参数的是`v`,参数的是`I``int`。您可以更改这些函数签名来查看其他可能的类型。
3. Now, let's look at how classes are compiled. Add the following code into **Compiler Explorer** under the existing code:
```cpp
class MyClass {
private:
int a = 5;
int myPrivateFunc(int i) {
a = 4;
return i + a;
}
public:
int b = 6;
int myFunc(){
return sum(1, myPrivateFunc(b));
}
};
MyClass myObject;
int main() {
myObject.myFunc();
}
```
以下是这些新增行的编译版本:

###### 图 1.46:编译版本
您可能会惊讶于编译代码中没有类定义。这些方法类似于全局函数,但有一点不同:它们的变形名称包含类名,并且它们接收对象实例作为参数。创建实例只是为类的字段分配空间。
在链接器阶段,这些损坏的函数名被用来匹配调用者和被调用者。对于找不到被调用方的调用方,我们会得到链接器错误。大多数链接器错误可以通过仔细检查源代码来解决。但是,在某些情况下,使用`objdump`查看对象文件内容有助于找到问题的根源。
## 调试 C++ 代码
在开发 C++ 项目时,您可能会遇到不同级别的问题:
* 首先,您可能会收到编译器错误。这可能是因为您在语法上犯了一个错误,或者对类型的错误选择,等等。编译器是你必须跳过的第一个环,它会捕捉到你可能犯的一些错误。
* 第二个环是接头。在那里,一个典型的错误是使用声明的东西,但没有实际定义。当您为库使用了错误的头文件时,这种情况经常发生,头文件会通告任何源文件或库中都不存在的特定签名。一旦你也跳过链接环,你的程序就可以执行了。
* 现在,下一个要跳过的环是避免任何运行时错误。您的代码可能已经正确编译和链接,但它可能正在做一些不起作用的事情,例如取消对空指针的引用或除以零。
要查找和修复运行时错误,您必须以某种方式与正在运行的应用进行交互并对其进行监控。一种常用的技术是向代码中添加`打印`语句,并监控它生成的日志,希望将应用行为与日志相关联,以查明代码中有问题的区域。虽然这适用于某些情况,但有时您需要更仔细地查看执行情况。
调试器是对抗运行时错误的更好工具。调试器可以让你一行一行地运行代码,继续运行并暂停在你想要的行上,调查内存的值,暂停在错误上,等等。这让您可以观察程序运行时内存中到底发生了什么,并识别导致不需要的行为的代码行。
`gdb`是可以调试 C++ 程序的规范命令行调试器。然而,这可能很难使用,因为调试本质上是一项可视化任务——您希望能够同时查看代码行、变量值和程序输出。幸运的是,Eclipse CDT 包含一个易于使用的可视化调试器。
### 练习 9:使用 Eclipse CDT 进行调试
您只是在运行您的项目并查看输出。现在你想学习如何详细调试你的代码。在本练习中,我们将探索 Eclipse CDT 的调试功能。执行以下步骤完成练习:
1. 在 Eclipse CDT 中打开 CMake 项目。
2. To ensure that we have an existing run configuration, click **Run** | **Run Configurations**. There, you should see a **CxxTemplate** entry under **C/C++ Application**.
#### 注意
既然我们之前运行了我们的项目,它应该在那里。如果没有,请返回并重新创建。
3. 关闭对话框继续。
4. To start the debugger, find the toolbar entry that looks like an insect (bug) and click on the dropdown next to it. Select **CxxTemplate** to debug the main application. If it asks you to switch to the debug perspective, accept. Now, this is what Eclipse will look like:

###### 图 1.47: Eclipse 调试屏幕
此时,我们的代码冻结在我们的`main()`函数的第一行,该函数在代码视图的中间用绿色高亮和箭头显示。在左边,我们看到正在运行的线程,其中只有一个。在右边,我们看到了在这种情况下可以访问的变量。在底部,我们看到了 Eclipse 在幕后实际调试可执行文件时使用的 **gdb** 输出。现在,我们的主要功能没有太多需要调试的地方。
5. 单击**运行**菜单下的**跳过**,或者在工具栏中单击几次,应用将很快终止。最后,你会看到`libc-start.c`库,它是`主`功能的调用者。完成后,您可以关闭它并切换到源文件。当你不再看到红色的停止按钮时,你就知道程序执行结束了。
6. Edit our `main` function by adding the following code:
```cpp
int i = 1, t = 0;
do {
t += i++ ;
} while (i <= 3);
std::cout << t << std::endl;
```
增量后操作符与偶尔的`do-while`循环混合在一起,对某些人来说可能会令人头疼。这是因为我们试图在头脑中执行算法。然而,我们的调试器完全能够一步一步地运行它,并向我们展示在执行过程中到底发生了什么。
7. Start debugging after adding the preceding code. Click on the dropdown next to the **Debug** button in the toolbar and select **CxxTemplate**. Press *F6* a couple of times to step over in the code. It will show us how the variables change as well as the line of code that will be executed next:

###### 图 1.48:跳过代码
8. Seeing the variables change after the execution of each line of code makes the algorithm much clearer to understand. As you press *F6*, note that the following are the values after each execution of the `t += i++ ;` line:

###### 图 1.49:随时间变化的状态
前面的输出清楚地解释了值是如何变化的,以及为什么在最后打印`6`。
9. Explore other features of the debugger. While the variable view is useful, you can also hover over any variable and browse its value:

###### 图 1.50:调试器的视图选项
此外,**表达式**视图可以帮助您计算从您浏览的值中不清楚的东西。
10. Click on **Expression** on the right-hand side and click on the **Add** button:

###### 图 1.51:添加表达式
11. Type **t+i** and hit *Enter*. Now you see the total in the list of expressions:

###### 图 1.52:带有新表达式的表达式视图
可以按工具栏中的红方,也可以选择**运行** | **终止**随时停止调试。另一个特性是断点,它告诉调试器每当它到达一个标有断点的行时就暂停。到目前为止,我们一直在一行行地遍历我们的代码,这在大型项目中可能非常耗时。相反,您通常希望继续执行,直到它到达您感兴趣的代码。
12. Now, instead of going line by line, add a breakpoint in the line that does the printing. For this, double-click on the area to the left of the line number of this line. In the following figure, the dot represents a breakpoint:

###### 图 1.53:使用断点
13. Now start the debugger. As usual, it will start paused. Now select **Run** | **Resume** or click on the toolbar button. It will run the three executions of the loop and pause at our breakpoint. This way, we saved time by stepping through code that we are not investigating:

###### 图 1.54:使用调试器
14. 当我们一直在处理我们添加的循环时,我们忽略了创建`应用`对象的线。**跳过**命令跳过了这一行。然而,我们也可以选择进入这一行的构造函数调用。为此,我们将使用**运行** | **进入**或相应的工具栏按钮。
15. Stop the debugger and start it again. Click on **Step Over** to go to the line where the application is created:

###### 图 1.55:使用调试器–单步执行选项
16. 突出显示的是下一行,如果我们再次单步执行,它将被执行。相反,请按“进入”按钮。这将带我们进入构造函数调用:

###### 图 1.56:使用调试器——单步执行选项
这是一个方便的功能,可以更深入地了解函数,而不是简单地跳过它。另外,请注意左侧调试视图中的调用堆栈。您可以随时点击下方的尝试再次查看呼叫者的上下文。
这是对 Eclipse CDT 调试器的简单介绍,它在引擎盖下使用 GDB 给你一个可视化的调试体验。当试图更好地理解运行时错误并纠正导致错误的错误时,您可能会发现调试很有用。
## 编写可读代码
虽然可视化调试器在识别和消除运行时错误或意外的程序行为方面非常有用,但是最好编写一开始就不太可能有问题的代码。做到这一点的一种方法是努力编写更容易阅读和理解的代码。然后,在代码中发现问题变得更像是识别英语句子之间的矛盾,而不是解决神秘的谜题。当你以一种可以理解的方式编写代码时,你的错误往往会在你编写代码时变得很明显,当你回来解决漏掉的问题时,你会更容易发现。
经过一些不愉快的维护经历,你意识到你写的程序的主要目的不是让计算机做你想做的事情,而是告诉读者当程序运行时计算机会做什么。这通常意味着您需要做更多的打字工作,IDEs 可以提供帮助。这也可能意味着您有时编写的代码在执行时间或使用的内存方面不是最佳的。如果这与你所学的相违背,考虑一下你可能用极少量的效率来换取不正确的风险。由于我们拥有巨大的处理能力和内存,您可能会让您的代码变得不必要的神秘,并且在徒劳地追求效率的过程中可能会出错。在接下来的部分中,我们将列出一些经验法则,这些法则可能有助于您编写可读性更强的代码。
### 缩进和格式化
与许多其他编程语言一样,C++ 代码由程序块组成。一个函数有一组语句,这些语句构成了它的块。循环的块语句将在迭代中执行。如果给定条件为真,则执行`if`语句块,否则执行相应的`else`语句块。
花括号,或者单语句块没有花括号,通知计算机,而空格形式的缩进通知人类读者关于块结构。缺少缩进,或者误导性缩进,会使读者很难理解代码的结构。因此,我们应该努力保持代码的良好缩进。考虑以下两个代码块:
```cpp
// Block 1
if (result == 2)
firstFunction();
secondFunction();
// Block 2
if (result == 2)
firstFunction();
secondFunction();
```
虽然它们在执行上是相同的,但是在第二个中更清楚的是`firstFunction()`只有在`结果`为`2`时才会执行。现在考虑以下代码:
```cpp
if (result == 2)
firstFunction();
secondFunction();
```
这简直是误导。如果读者不小心,他们可能很容易认为只有当`结果`为`2`时,才会执行`secondFunction()`。但是,这段代码在执行方面与前面两个示例完全相同。
如果你觉得修改缩进会让你慢下来,你可以使用编辑器的格式化工具来帮助你。在 Eclipse 中,您可以选择一段代码并使用 **Source** | **修正缩进**来修复该选择的缩进,或者使用 **Source** | **Format** 来修复代码的其他格式问题。
除了缩进之外,其他格式规则,如将大括号放在正确的行,在二进制运算符周围插入空格,以及在每个逗号后插入空格,也是非常重要的格式规则,您应该遵守这些规则来保持代码格式良好且易于阅读。
在 Eclipse 中,您可以在**窗口** | **首选项** | **C/C++** | **代码样式** | **格式化程序**或在**项目** | **属性** | **C/C++ 常规** | **格式化程序**中设置每个工作区的格式化规则。您可以选择一种行业标准样式,如 K & R 或 GNU,也可以修改它们并创建自己的样式。当您使用**源代码** | **格式**来格式化您的代码时,这变得尤为重要。例如,如果您选择使用空格进行缩进,但是 Eclipse 的格式规则设置为制表符,那么您的代码将变成制表符和空格的混合。
### 使用有意义的名称作为标识符
在我们的代码中,我们使用标识符来命名许多项目——变量、函数、类名、类型等等。对于计算机来说,这些标识符只是一个字符序列,用来区分它们。然而,对于读者来说,它们要多得多。标识符应该完整而明确地描述它所代表的项目。同时,它不应该太长。此外,它应该遵守正在使用的风格标准。
考虑以下代码:
```cpp
studentsFile File = runFileCheck("students.dat");
bool flag = File.check();
if (flag) {
int Count_Names = 0;
while (File.CheckNextElement() == true) {
Count_Names += 1;
}
std::cout << Count_Names << std::endl;
}
```
虽然这是一段完全有效的 C++ 代码,但很难阅读。让我们列出它的问题。首先,我们来看看标识符的样式问题。`学生文件`类名以小写字母开头,应该改为大写。`文件`变量应该以小写字母开头。`Count_Names`变量应该以小写字母开头,不应该有下划线。`CheckNextElement`方法应该以小写字母开头。虽然这些规则看起来很随意,但是命名的一致性带来了关于名称的额外信息——当你看到一个以大写字母开头的单词时,你马上就会明白它一定是一个类名。此外,使用不符合标准的名字只会分散注意力。
现在,让我们超越风格,检查名称本身。第一个有问题的名字是`runFileCheck`函数。方法是一个返回值的动作:它的名字应该清楚地解释它做什么以及它返回什么。“检查”是一个过度使用的词,对大多数情况来说太模糊了。是的,我们检查过了,它就在那里——那我们该怎么办?在这种情况下,似乎我们实际上读取了文件并创建了一个`文件`对象。在这种情况下,`运行文件检查`应该改为`读取文件`。这清楚地解释了正在采取的行动,而返回值正是您所期望的。如果您想更具体地了解返回值,`readAsFile`可能是另一种选择。同样的,`检查`的方法也比较模糊,应该是`存在`代替。`CheckNextElement`方法也比较模糊,应该是`next elements`代替。
另一个被过度使用的模糊词是`标志`,常用于布尔变量。这个名字暗示了一种开/关的情况,但没有给出它的价值意味着什么的线索。在这种情况下,其`真`值表示文件存在,`假`值表示文件不存在。命名布尔变量的技巧是,当变量的值为`真`时,设计一个正确的问题或语句。在本例中,`文件存在`和`文件不存在`是两个不错的选择。
我们的下一个错误命名的变量是`Count_Names`,或`countNames`,其大写正确。对于整数来说,这是一个不好的名字,因为这个名字并没有暗示一个数字——它暗示了一个产生数字的动作。取而代之的是,像`numNames`或`nameCount`这样的标识符可以清楚地传达出里面的数字是什么意思。
### 保持算法清晰简单
当我们阅读代码时,所采取的步骤和流程应该是有意义的。间接完成的事情——函数的副产品,以效率的名义一起完成的多个动作,等等——让读者很难理解你的代码。例如,让我们看看下面的代码:
```cpp
int *input = getInputArray();
int length = getInputArrayLength();
int sum = 0;
int minVal = 0;
for (int i = 0; i < length; ++ i) {
sum += input[i];
if (i == 0 || minVal > input[i]) {
minVal = input[i];
}
if (input[i] < 0) {
input[i] *= -1;
}
}
```
这里,我们有一个在循环中处理的数组。乍一看,不太清楚这个循环到底在做什么。变量名有助于我们理解正在发生的事情,但是我们必须在头脑中运行算法,以确保这些名字所宣传的东西确实发生在这里。这个循环中有三种不同的操作。首先,我们要找到所有元素的总和。其次,我们正在寻找数组中的最小元素。第三,我们取这些运算后每个元素的绝对值。
现在考虑这个替代版本:
```cpp
int *input = getInputArray();
int length = getInputArrayLength();
int sum = 0;
for (int i = 0; i < length; ++ i) {
sum += input[i];
}
int minVal = 0;
for (int i = 0; i < length; ++ i) {
if (i == 0 || minVal > input[i]) {
minVal = input[i];
}
}
for (int i = 0; i < length; ++ i) {
if (input[i] < 0) {
input[i] *= -1;
}
}
```
现在一切都清楚多了。第一个循环找到输入的总和,第二个循环找到最小元素,第三个循环找到每个元素的绝对值。虽然它更清晰,更容易理解,但您可能会觉得自己在做三个循环,因此浪费了 CPU 资源。创建更高效代码的动力可能会迫使您合并这些循环。请注意,您在这里获得的效率提升微乎其微;你的程序的时间复杂度仍然是 O(n)。
在创建代码时,可读性和效率是两个经常竞争的限制因素。如果你想开发可读和可维护的代码,你应该优先考虑可读性。然后,您应该努力开发同样高效的代码。否则,可读性低的代码有难以维护的风险,或者更糟的是,有难以识别和修复的错误的风险。当你的程序产生不正确的结果时,或者当增加新功能的成本变得太高时,你的程序的高效率将变得无关紧要。
### 练习 10:使代码可读
下面的代码中有样式和缩进问题。空格使用不一致,缩进不正确。此外,关于单语句`的决定,如果`块是否有花括号是不一致的。下面这段代码在缩进、格式、命名和清晰度方面有问题:
```cpp
//a is the input array and Len is its length
void arrayPlay(int *a, int Len) {
int S = 0;
int M = 0;
int Lim_value = 100;
bool flag = true;
for (int i = 0; i < Len; ++ i) {
S += a[i];
if (i == 0 || M > a[i]) {
M = a[i];
}
if (a[i] >= Lim_value) { flag = true;
}
if (a[i] < 0) {
a[i] *= 2;
}
}
}
```
让我们修复这些问题,并使其与常见的 C++ 代码风格兼容。执行以下步骤完成本练习:
1. 打开 Eclipse CDT。
2. Create a new **ArrayPlay.cpp** file in the **src** folder and paste the preceding code. Make sure you do not have any text selected. Then, go to **Source** | **Format** from the top menu and accept the dialog to format the entire file. This makes our code look like the following:
```cpp
//a is the input array and Len is its length
void arrayPlay(int *a, int Len) {
int S = 0;
int M = 0;
int Lim_value = 100;
bool flag = true;
for (int i = 0; i < Len; ++ i) {
S += a[i];
if (i == 0 || M > a[i]) {
M = a[i];
}
if (a[i] >= Lim_value) {
flag = true;
}
if (a[i] < 0) {
a[i] *= 2;
}
}
}
```
现在代码更容易理解了,让我们试着理解它的作用。感谢评论,我们了解到我们有一个输入数组`a`,长度为`Len`。更好的名字是`输入`和`输入`。
3. 让我们进行第一个更改,并将`a`重命名为`输入`。如果您正在使用 Eclipse,您可以选择**重构** | **重命名**来重命名一个事件,所有其他事件也将被重命名。对`透镜`进行同样的操作,并将其重命名为`输入长度`。
4. 更新后的代码如下所示。请注意,我们不再需要注释,因为参数名称是不言自明的:
```cpp
void arrayPlay(int *input, int inputLength) {
int S = 0;
int M = 0;
int Lim_value = 100;
bool flag = true;
for (int i = 0; i < inputLength; ++ i) {
S += input[i];
if (i == 0 || M > input[i]) {
M = input[i];
}
if (input[i] >= Lim_value) {
flag = true;
}
if (input[i] < 0) {
input[i] *= 2;
}
}
}
```
5. 我们在循环之前定义了几个其他变量。让我们试着理解他们。似乎它对`S`所做的一切就是给它添加每个元素。所以,`S`一定是`sum`。另一方面,`M`似乎是最小的元素——我们把它命名为`最小的`。
6. `Lim_value` seems to be a threshold, where we simply want to know whether it has been crossed. Let's rename it `topThreshold`. The `flag` variable is set to true if this threshold is crossed. Let's rename it to `isTopThresholdCrossed`. Here is the state of the code after these changes with **Refactor** | **Rename**:
```cpp
void arrayPlay(int *input, int inputLength) {
int sum = 0;
int smallest = 0;
int topThreshold = 100;
bool isTopThresholdCrossed = true;
for (int i = 0; i < inputLength; ++ i) {
sum += input[i];
if (i == 0 || smallest > input[i]) {
smallest = input[i];
}
if (input[i] >= topThreshold) {
isTopThresholdCrossed = true;
}
if (input[i] < 0) {
input[i] *= 2;
}
}
}
```
现在,让我们看看如何让这段代码更简单、更容易理解。前面的代码正在做这些事情:计算输入元素的总和,找到最小的一个,确定是否超过了最高阈值,并将每个元素乘以 2。
7. 由于所有这些都是在同一个循环中完成的,所以算法现在不是很清楚。修复它,并有四个独立的循环:
```cpp
void arrayPlay(int *input, int inputLength) {
// find the sum of the input
int sum = 0;
for (int i = 0; i < inputLength; ++ i) {
sum += input[i];
}
// find the smallest element
int smallest = 0;
for (int i = 0; i < inputLength; ++ i) {
if (i == 0 || smallest > input[i]) {
smallest = input[i];
}
}
// determine whether top threshold is crossed
int topThreshold = 100;
bool isTopThresholdCrossed = true;
for (int i = 0; i < inputLength; ++ i) {
if (input[i] >= topThreshold) {
isTopThresholdCrossed = true;
}
}
// multiply each element by 2
for (int i = 0; i < inputLength; ++ i) {
if (input[i] < 0) {
input[i] *= 2;
}
}
}
```
现在代码清晰多了。虽然很容易理解每个块在做什么,但我们也添加了注释,使其更加清晰。在这一节中,我们更好地理解了我们的代码是如何转换成可执行文件的。然后,我们讨论了用代码识别和解决可能的错误的方法。我们最后讨论了如何编写不太可能有问题的可读代码。在下一节中,我们将解决一个活动,其中我们将使代码更易读。
### 活动 3:提高代码可读性
您可能有不可读且包含 bug 的代码,要么是因为您匆忙编写的,要么是从其他人那里收到的。您希望更改代码以消除其错误并使其更易读。我们有一段代码需要改进。逐步改进它,并使用调试器解决问题。执行以下步骤来实施本活动:
1. 下面你会找到 **SpeedCalculator.cpp** 和 **SpeedCalculator.h** 的来源。它们包含`速度计算器`类。将这两个文件添加到您的项目中。
2. 在你的`main()`函数中创建这个类的一个实例,并调用它的`run()`方法。
3. 修复代码中的样式和命名问题。
4. 简化代码,使其更容易理解。
5. 运行代码并在运行时观察问题。
6. 使用调试器来解决问题。
这是您将添加到项目中的**速度计算器. cpp** 和**速度计算器. h** 的代码。作为本活动的一部分,您将修改它们:
```cpp
// SpeedCalculator.h
#ifndef SRC_SPEEDCALCULATOR_H_
#define SRC_SPEEDCALCULATOR_H_
class SpeedCalculator {
private:
int numEntries;
double *positions;
double *timesInSeconds;
double *speeds;
public:
void initializeData(int numEntries);
void calculateAndPrintSpeedData();
};
#endif /* SRC_SPEEDCALCULATOR_H_ */
```
```cpp
//SpeedCalculator.cpp
#include "SpeedCalculator.h"
#include
#include
#include
#include
void SpeedCalculator::initializeData(int numEntries) {
this->numEntries = numEntries;
positions = new double[numEntries];
timesInSeconds = new double[numEntries];
srand(time(NULL));
timesInSeconds[0] = 0.0;
positions[0] = 0.0;
for (int i = 0; i < numEntries; ++ i) {
positions[i] = positions[i-1] + (rand()%500);
timesInSeconds[i] = timesInSeconds[i-1] + ((rand()%10) + 1);
}
}
void SpeedCalculator::calculateAndPrintSpeedData() {
double maxSpeed = 0;
double minSpeed = 0;
double speedLimit = 100;
double limitCrossDuration = 0;
for (int i = 0; i < numEntries; ++ i) {
double dt = timesInSeconds[i+1] - timesInSeconds[i];
assert (dt > 0);
double speed = (positions[i+1] - positions[i]) / dt;
if (maxSpeed < speed) {
maxSpeed = speed;
}
if (minSpeed > speed) {
minSpeed = speed;
}
if (speed > speedLimit) {
limitCrossDuration += dt;
}
speeds[i] = speed;
}
std::cout << "Max speed: " << maxSpeed << std::endl;
std::cout << "Min speed: " << minSpeed << std::endl;
std::cout << "Total duration: " <<
timesInSeconds[numEntries - 1] - timesInSeconds[0] << " seconds" << std::endl;
std::cout << "Crossed the speed limit for " << limitCrossDuration << " seconds"<< std::endl;
delete[] speeds;
}
```
#### 注意
这项活动的解决方案可以在第 626 页找到。
## 总结
在本章中,我们学习了如何创建可移植和可维护的 C++ 项目。我们首先学习了如何创建 CMake 项目,以及如何将它们导入到 Eclipse CDT 中,让我们可以选择使用命令行还是 IDE。本章的其余部分集中在消除我们项目中的各种问题。首先,我们学习了如何将单元测试添加到项目中,以及如何使用它们来确保我们的代码按预期工作。我们继续讨论了代码的编译和链接步骤,并观察了目标文件的内容,以便更好地理解可执行文件。然后,我们学习了如何在 IDE 中可视化地调试代码,以消除运行时错误。我们用一些帮助创建可读、可理解和可维护的代码的经验法则结束了这次讨论。这些方法将在你的 C++ 之旅中派上用场。在下一章中,我们将了解更多关于 C++ 的类型系统和模板。
================================================
FILE: docs/adv-cpp/02.md
================================================
# 二、不允许鸭子——类型和推导(一)
## 学习目标
本章结束时,您将能够:
* 实现您自己的行为类似于内置类型的类
* 实现控制编译器创建哪些函数的类(零规则/五规则)
* 像往常一样,使用自动变量开发函数
* 通过使用强类型编写更安全的代码来实现类和函数
本章将为您提供一个良好的 C++ 类型系统基础,并允许您编写自己的类型在该系统中工作。
## 简介
C++ 是一种强类型、静态类型的语言。编译器使用与所使用的变量及其上下文相关的类型信息来检测和防止某些类别的编程错误。这意味着每个对象都有一个类型,并且该类型永远不会改变。相比之下,动态类型语言(如 Python 和 PHP)将这种类型检查推迟到运行时(也称为后期绑定),变量的类型可能会在应用执行过程中发生变化。这些语言使用鸭子测试而不是变量类型——也就是说,“如果它像鸭子一样走路和说话,那么它一定是一只鸭子。”静态类型的语言,如 C++ 依赖于类型来确定变量是否可以用于给定的目的,而动态类型的语言依赖于某些方法和属性的存在来确定其适用性。
C++ 最初被描述为“带类的 C”。这是什么意思?基本上,C 语言提供了一组内置的基本类型——int、float、char 等等——以及这些项的指针和数组。您可以使用结构将它们聚合到相关项的数据结构中。C++ 将此扩展到类,这样您就可以用操作符完全定义自己的类型,从而使它们成为语言中的一流公民。从最初的卑微开始,C++ 已经发展成为不仅仅是“带类的 C”,因为它现在可以表达面向对象的范式(封装、多态、抽象和继承)、函数范式和泛型编程(模板)。
在这本书里,我们将关注 C++ 支持面向对象范式意味着什么。随着您作为开发人员的经验的增长,以及您接触到 Clojure、Haskell、Lisp 和其他函数式语言,它们将帮助您编写健壮的 C++ 代码。像 Python、PHP 和 Ruby 这样的动态类型语言已经影响了我们编写 C++ 代码的方式。随着 C++ 17 的到来,引入了`std::variant`类——一个保存我们选择的任何类型(在编译时)的类,其行为非常像动态语言中的变量。
在前一章中,我们学习了如何使用 CMake 创建可移植和可维护的 C++ 项目。我们学习了如何在项目中加入单元测试来帮助编写正确的代码,以及如何在问题出现时进行调试。我们学习了工具链如何获取我们的代码,并通过程序管道运行它来生成可执行文件。最后,我们总结了一些帮助我们创建可读、可理解和可维护代码的经验法则。
在这一章中,我们将对 C++ 类型系统进行一次旋风式的旅行,一边走一边声明和使用我们自己的类型。
## C++ 类型
作为一种强类型和静态类型的语言,C++ 提供了几种基本类型,并且能够定义自己的类型,并根据需要提供或多或少的功能来解决手头的问题。本节将首先介绍基本类型,初始化它们,声明一个变量,并将一个类型与之相关联。然后,我们将探讨如何声明和定义一个新类型。
### C++ 基本类型
C++ 包括几个*基本类型*,或者*内置类型*。C++ 标准定义了每种类型的最小内存大小及其相对大小。编译器识别这些基本类型,并有内置的规则来定义哪些操作可以在这些类型上执行,哪些不能。类型之间的隐式转换也有规则;例如,从 int 类型转换为 float 类型。
#### 注意
参见[https://en.cppreference.com/w/cpp/language/types](https://en.cppreference.com/w/cpp/language/types)的**基本类型**部分,了解所有内置类型的简要说明。
### C++ 文字
C++ 文字用于告诉编译器,当您声明变量或赋值给变量时,您希望与变量相关联的值。上一节中的每个内置类型都有一种与之关联的文字形式。
#### 注意
参见[https://en.cppreference.com/w/cpp/language/expressions](https://en.cppreference.com/w/cpp/language/expressions)的**文字**部分,了解每种类型文字的简要说明。
## 指定类型–变量
由于 C++ 是一种静态类型的语言,所以在声明变量时需要指定变量的类型。当您声明一个函数时,有必要指定返回类型和传递给它的参数类型。在声明变量时,有两种方法可以指定变量的类型:
* **明确地**:你作为程序员,正在精确地规定类型是什么。
* **隐式**(使用 auto):您告诉编译器查看用于初始化变量的值并确定其类型。这就是众所周知的(自动)**式推演**。
标量变量的一般声明形式如下:
```cpp
type-specifier var; // 1\. Default-initialized variable
type-specifier var = init-value; // 2\. Assignment initialized variable
type-specifier var{init-value}; // 3\. Brace-initialize variable
```
`类型说明符`表示您希望与`变量`关联的类型(基本或用户定义的)。所有这三种形式都会导致编译器分配一些存储来保存值,并且所有将来对`变量`的引用都将引用该位置。`初始化值`用于初始化存储位置。默认初始化对内置类型不起任何作用,并将根据函数重载解析调用用户定义类型的构造函数来初始化存储。
编译器必须知道要分配多少内存,并提供一个运算符来确定一个类型或变量有多大–`size of`。
根据我们的声明,编译器将在计算机内存中留出空间来存储变量引用的数据项。考虑以下声明:
```cpp
int value = 42; // declare value to be an integer and initialize to 42
short a_value{64}; // declare a_value to be a short integer and initialize
// to 64
int bad_idea; // declare bad_idea to be an integer and DO NOT
// initialize it. Use of this variable before setting
// it is UNDEFINED BEHAVIOUR.
float pi = 3.1415F; // declare pi to be a single precision floating point
// number and initialize it to pi.
double e{2.71828}; // declare e to be a double precision floating point
// number and initialize it to natural number e.
auto title = "Sir Robin of Loxley"; // Let the compiler determine the type
```
如果这些是在函数的范围内声明的,那么编译器会从所谓的堆栈中为它们分配内存。这方面的内存布局可能如下所示:

###### 图 2A.1:变量的内存布局
编译器将按照我们声明变量的顺序分配内存。之所以会出现未使用的内存,是因为编译器会分配内存,这样基本类型通常会被自动访问,并与适当的内存边界对齐以提高效率。注意`标题`是`const char *`类型,这是一个**指针**,我们接下来将与`const`一起讨论。**“洛克斯利的罗宾爵士”**字符串将存储在加载程序时初始化的内存的不同部分。我们稍后将讨论程序内存。
标量声明语法的轻微修改为我们提供了声明值数组的语法:
```cpp
type-specifier ary[count]; // 1\. Default-initialized
type-specifier ary[count] = {comma-separated list}; // 2\. Assignment initialized
type-specifier ary[count]{comma-separated list}; // 3\. Brace-initialized
```
对于多维数组,可以这样做:
```cpp
type-specifier ary2d[countX][countY];
type-specifier ary3d[countX][countY][countZ];
// etc...
```
请注意,`count`、`countX`和前面声明中的其他项目必须在编译时计算为常数,否则将导致错误。此外,逗号分隔的初始值设定项列表中的项数必须小于或等于`计数`,否则将再次出现编译错误。在下一节中,我们将应用到目前为止在练习中学到的概念。
#### 注意
在解决本章中的任何实际问题之前,请下载本书的 GitHub 资源库([https://github.com/TrainingByPackt/Advanced-CPlusPlus](https://github.com/TrainingByPackt/Advanced-CPlusPlus))并导入 Eclipse 中的 2A 课文件夹,以便您可以查看每个练习和活动的代码。
### 练习 1:声明变量和探索大小
本练习将设置本章的所有练习,然后让您熟悉声明和初始化内置类型的变量。还将向您介绍**自动申报**、**阵列**和的**尺寸。让我们开始吧:**
1. 打开 Eclipse(用在*第 1 章*、*便携式 C++ 软件剖析*中),如果出现启动器窗口,点击启动。
2. 转到**文件**,在**新建****下选择**项目……**,转到选择 C++ 项目(不是 C/C++ 项目)。**
*** 点击**下一步>** ,清除**使用默认位置**复选框,输入**第二课**作为**项目名称**。* 选择**项目类型**的**空项目**。然后,点击**浏览……**并导航到包含第 2 课示例的文件夹。* 点击**打开**选择文件夹并关闭对话框。* 点击**下一步>** 、**下一步>** ,然后**完成**。* 为了帮助您完成练习,我们将配置工作区,以便在构建之前自动保存文件。进入**窗口**,选择**偏好设置**。在**通用**下,打开**工作区**,选择**构建**。* 在构建之前,勾选**自动保存框,然后点击**应用并关闭**。*** 就像*章**1**解析可移植 C++ 软件*一样,这是一个基于 CMake 的项目,所以我们需要改变当前的构建器。点击**项目浏览器**中的**第 2 课**,然后点击**项目**菜单下的**属性**。从左窗格中选择 C/C++ 构建下的工具链编辑器,并将当前构建器设置为 Cmake 构建(可移植)。* Click **Apply and Close**. Then, choose the **Project** | **Build All** menu item to build all the exercises. By default, the console at the bottom of the screen will display the **CMake Console [Lesson2A]**:

###### 图 2A.2: CMake 控制台输出
* In the top-right corner of the console, click on the **Display Selected Console** button and then select **CDT Global Build Console** from the list:

###### 图 2A.3:选择不同的控制台
这将显示构建的结果—它应该显示 0 个错误和 3 个警告:

###### 图 2A.4:构建过程控制台输出
* As the build was successful, we want to run Exercise1\. At the top of the window, click on the drop-down list where it says **No Launch Configurations**:

###### 图 2A.5:启动配置菜单
* 点击**新启动配置…** 。保持默认值不变,点击**下一步>T3。*** Change **Name** to **Exercise1** and then click **Search Project**:

###### 图 2A.6:练习 1 启动配置
* 从二进制文件窗口显示的程序列表中,单击**练习 1** 并单击**确定**。* Click **Finish**. This will result in exercise1 being displayed in the Launch Configuration drop-down box:

###### 图 2A.7:更改启动配置
* To run **Exercise1**, click on the **Run** button. Exercise1 will execute and display its output in the console:

###### 图 2A.8:练习 1 的输出
这个程序没有任何价值——它只是在你的系统上输出各种类型的大小。但这说明程序是有效的,可以编译。请注意,系统的数字可能不同(尤其是 sizeof(title)值)。
* In the **Project Explorer**, expand **Lesson2A**, then **Exercise01**, and double-click on **Exercise1.cpp** to open the file for this exercise in the editor:
```cpp
int main(int argc, char**argv)
{
std::cout << "\n\n------ Exercise 1 ------\n";
int value = 42; // declare value to be an integer & initialize to 42
short a_value{64}; // declare a_value to be a short integer &
// initialize to 64
int bad_idea; // declare bad_idea to be an integer and DO NOT
// initialize it. Use of this variable before
// setting it is UNDEFINED BEHAVIOUR.
float pi = 3.1415F; // declare pi to be a single precision floating
// point number and initialize it to pi.
double e{2.71828}; // declare e to be a double precision floating point
// number and initialize it to natural number e.
auto title = "Sir Robin of Loxley";
// Let the compiler determine the type
int ary[15]{}; // array of 15 integers - zero initialized
// double pi = 3.14159; // step 24 - remove comment at front
// auto speed; // step 25 - remove comment at front
// value = "Hello world";// step 26 - remove comment at front
// title = 123456789; // step 27 - remove comment at front
// short sh_int{32768}; // step 28 - remove comment at front
std::cout << "sizeof(int) = " << sizeof(int) << "\n";
std::cout << "sizeof(short) = " << sizeof(short) << "\n";
std::cout << "sizeof(float) = " << sizeof(float) << "\n";
std::cout << "sizeof(double) = " << sizeof(double) << "\n";
std::cout << "sizeof(title) = " << sizeof(title) << "\n";
std::cout << "sizeof(ary) = " << sizeof(ary)
<< " = " << sizeof(ary)/sizeof(ary[0])
<< " * " << sizeof(ary[0]) << "\n";
std::cout << "Complete.\n";
return 0;
}
```
关于前面的程序需要注意的一点是,main 函数的第一条语句实际上是一条可执行语句,而不是声明。C++ 允许你在任何地方声明一个变量。它的前身 C 最初要求所有变量必须在任何可执行语句之前声明。
#### 最佳实践
声明一个尽可能接近它将被使用的地方的变量并初始化它。
* 在编辑器中,通过删除行首的分隔符(`//`)取消标记为`步骤 24`的行的注释:
```cpp
double pi = 3.14159; // step 24 - remove comment at front
// auto speed; // step 25 - remove comment at front
// value = "Hello world";// step 26 - remove comment at front
// title = 123456789; // step 27 - remove comment at front
// short sh_int{32768}; // step 28 - remove comment at front
```
* Click on the **Run** button again. This will cause the program to be built again. This time, the build will fail with an error:

###### 图 2A.9:工作空间对话框中的错误
* Click on **Cancel** to close the dialog. If **CDT Build Console [Lesson2A]** is not displayed, then select it as the active console:

###### 图 2A.10:重复声明错误
这一次,构建失败了,因为我们试图重新定义变量的类型,即 pi。编译器会给出有用的信息,告诉我们需要在哪里进行修复。
* 将注释分隔符恢复到行首。在编辑器中,通过删除行首的分隔符(//)取消标记为`步骤 25`的行的注释:
```cpp
// double pi = 3.14159; // step 24 - remove comment at front
auto speed; // step 25 - remove comment at front
// value = "Hello world";// step 26 - remove comment at front
// title = 123456789; // step 27 - remove comment at front
// short sh_int{32768}; // step 28 - remove comment at front
```
* Click on the **Run** button again. When the Errors in Workspace dialog appears, click **Cancel**:

###### 图 2A.11:自动声明错误-没有初始化
同样,构建失败了,但是这一次,我们没有给编译器足够的信息来推断速度的类型——自动类型变量必须被初始化。
* 将注释分隔符恢复到行首。在编辑器中,通过删除行首的注释起始分隔符(//),取消标记为`步骤 26`的行的注释:
```cpp
// double pi = 3.14159; // step 24 - remove comment at front
// auto speed; // step 25 - remove comment at front
value = "Hello world";// step 26 - remove comment at front
// title = 123456789; // step 27 - remove comment at front
// short sh_int{32768}; // step 28 - remove comment at front
```
* Click on the **Run** button again. When the Errors in Workspace dialog appears, click **Cancel**:

###### 图 2A.12:将不正确的值类型分配给变量
这一次,构建失败了,因为我们试图将错误的数据类型,即“Hello world”,它是一个 const char*,分配给 int 类型的变量,即`值`。
* 将注释分隔符恢复到行首。在编辑器中,取消标记为`的行的注释步骤 27`,方法是删除行首的分隔符(//):
```cpp
// double pi = 3.14159; // step 24 - remove comment at front
// auto speed; // step 25 - remove comment at front
// value = "Hello world";// step 26 - remove comment at front
title = 123456789; // step 27 - remove comment at front
// short sh_int{32768}; // step 28 - remove comment at front
```
* Click on the **Run** button again. When the Errors in Workspace dialog appears, click **Cancel**:

###### 图 2A.13:将不正确的值类型分配给自动变量
同样,构建失败是因为我们试图将错误的数据类型,即类型为`int`的 123456789 分配给 title,这是一个`const char*`。这里需要注意的一件非常有用的事情是`标题`是用`自动`类型声明的。编译器生成的错误消息告诉我们标题被推断为`const char*`类型。
* 将注释分隔符恢复到行首。在编辑器中,通过删除行首的分隔符(//)取消标记为`步骤 28`的行的注释:
```cpp
// double pi = 3.14159; // step 24 - remove comment at front
// auto speed; // step 25 - remove comment at front
// value = "Hello world";// step 26 - remove comment at front
// title = 123456789; // step 27 - remove comment at front
short sh_int{32768}; // step 28 - remove comment at front
```
* Click on the **Run** button again. When the Errors in Workspace dialog appears, click **Cancel**:

###### 图 2A.14:赋值太大而无法放入变量
同样,构建失败,但这一次是因为我们试图用( **32768** )初始化`sh_int`的值不适合分配给`short`类型的内存。短的占用两个字节的内存,被认为是 16 位的有符号量。这意味着可以短时存储的数值范围是`-2^(16-1)`到`2^(16-1)-1`,或者 **-32768** 到 **32767** 。
* 将数值从 **32768** 更改为 **32767** ,点击**运行**按钮。这一次,程序编译并运行,因为该值可以用一个`简称`来表示。* 将数值从 **32767** 更改为 **-32768** ,点击**运行**按钮。同样,程序编译并运行,因为该值可以用一个`简称`来表示。* 将注释分隔符恢复到行首。在编辑器中,进行您能想到的任何更改,使用任何基本类型及其关联的文字来探索变量声明,然后根据需要经常单击**运行**按钮。检查生成控制台中的输出是否有任何错误消息,因为这可能有助于您找到错误。**
**在本练习中,我们学习了如何设置 Eclipse 开发、实现变量声明以及解决声明问题。
## 指定类型–功能
既然我们可以将一个变量声明为某种类型,我们就需要对这些变量做些什么。在 C++ 中,我们通过调用函数来做事情。函数是传递结果的一系列语句。这个结果可以是一个数学计算(例如指数),然后发送到一个文件或写入一个终端。
函数允许我们将解决方案分解成更容易管理和理解的语句序列。当我们编写这些打包的语句时,我们可以在有意义的地方重用它们。如果我们需要它根据上下文以不同的方式运行,那么我们会传递一个参数。如果它返回一个结果,那么函数需要一个返回类型。
由于 C++ 是一种强类型语言,我们需要指定与我们实现的函数相关的类型——函数返回的值的类型(包括不返回)和传递给它的参数的类型(如果有的话)。
以下是一个典型的 hello world 程序:
```cpp
#include
void hello_world()
{
std::cout << "Hello world\n";
}
int main(int argc, char** argv)
{
std::cout << "Starting program\n";
hello_world();
std::cout << "Exiting program\n";
return 0;
}
```
前面的例子中已经声明了两个函数–`hello _ world()`和`main()`。`main()`函数是所有 C++ 程序的入口点,它返回一个传递给主机系统的`int`值。它被称为出口代码。
从返回类型的声明到左大括号({)的所有内容都被称为**函数原型**。它定义了三件事,即返回类型、函数名以及参数的数量和类型。
对于第一个函数,返回类型为`void`–即不返回值;它的名字是`hello_world`,没有任何争议:

###### 图 2A.15:声明一个不接受参数也不返回任何内容的函数
第二个函数返回一个`int`值,名称为`main`,并接受两个参数。这些参数分别是`argc`和`argv`,并且使`int`和*指针分别指向* `char`类型的指针:

###### 图 2A.16:接受两个参数并返回一个整数的函数的声明
功能原型之后的一切被称为**功能体**。函数体包含变量声明和要执行的语句。
函数必须在使用前声明,也就是说,编译器需要知道它的参数和返回类型。如果函数是在文件中定义的,并且在调用该函数后将在该文件中使用该函数,则可以通过在使用该函数之前提供该函数的前向声明来解决这个问题。
前向声明是通过在调用函数原型之前将以分号结束的函数原型放入文件中来实现的。对于`hello_world()`,这将按如下方式完成:
```cpp
void hello_world();
```
对于主要功能,这将按如下方式完成:
```cpp
int main(int, char**);
```
函数原型不需要参数的名称,只需要类型。但是,为了帮助该功能的用户,保留它们是一个好主意。
在 C++ 中,函数的定义可以在一个文件中,需要从不同的文件中调用。那么,第二个文件如何知道它希望调用的函数的原型呢?这是通过将正向声明放入一个单独的文件(称为头文件)并将其包含在第二个文件中来实现的。
### 练习 2:声明函数
在本练习中,我们将测试编译器在遇到函数调用并实现前向声明以解析未知函数时需要知道什么。我们开始吧。
1. 在 Eclipse 中打开**第 2 课**项目,然后在**项目浏览器**中,展开**第 2 课**,然后展开**练习 02** ,双击**练习 2.cpp** 将本练习的文件打开到编辑器中。
2. 点击**启动配置**下拉菜单,选择**新启动配置……**。
3. 将**练习 2** 配置为以**练习 2** 的名称运行。完成后,它将是当前选定的启动配置。
4. Click on the **Run** button. Exercise 2 will run and produce the following output:

###### 图 2A.17:练习 2 程序的输出
5. 进入编辑器,通过移动`gcd`功能更改代码,使其位于`主`之后。应该是这样的:
```cpp
int main(int argc, char**argv)
{
std::cout << "\n\n------ Exercise 2 ------\n";
std::cout << "The greatest common divisor of 44 and 121 is " << gcd(44, 121) << "\n";
std::cout << "Complete.\n";
return 0;
}
int gcd(int x, int y)
{
while(y!=0)
{
auto c{x%y};
x = y;
y = c;
}
return x;
}
```
6. Click on the **Run** button again. When the Errors in Workspace dialog appears, click **Cancel**. In the **CDT Build Console [Lesson2A]**, we will see the reason for the failure:

###### 图 2A.18:由于未定义的功能导致的构建失败
这一次,编译器不知道如何处理对`gcd()`函数的调用。它在需要调用函数的时候并不知道这个函数,即使它是在同一个文件中定义的,但是在调用之后。
7. 在编辑器中,将 forward 声明放在主函数定义之前。还要加一个分号(;)结尾:
```cpp
int gcd(int x, int y);
```
8. 再次点击**运行**按钮。这一次,程序编译并恢复原始输出。
在本练习中,我们学习了如何转发声明函数,以及如何解决在使用函数之前未声明函数时出现的编译器错误。
在早期版本的 C 编译器中,这是可以接受的。程序会假设该函数存在,并返回一个 int。函数的参数可以从调用中推断出来。然而,在现代 C++ 的情况下,这是不正确的,因为在使用它之前,您必须声明一个函数、类、变量等等。在下一节中,我们将学习指针类型。
### 指针类型
因为它起源于 C 语言,也就是说,编写最佳效率的系统并直接访问硬件,C++ 允许您将变量声明为指针类型。它的格式如下:
```cpp
type-specifier* pvar = &var;
```
这和以前一样,除了两件事:
* 使用特殊声明符星号(`*`)来指示名为 pvar 的变量指向内存中的位置或地址。
* 它使用特殊运算符&符号( **&** )进行初始化,在这种情况下,它告诉编译器返回 **var** 变量的地址。
由于 C 是一种高级语言,但具有低级访问,指针允许用户直接访问内存,这在我们希望向硬件提供输入/输出并因此控制它时很有帮助。指针的另一个用途是允许向函数提供对公共数据项的访问,并消除调用函数时复制大量数据的需要,因为它默认为按值传递。要访问指针所指向的值,特殊运算符星号(`*`)用于**取消引用**位置:
```cpp
int five = 5; // declare five and initialize it
int *pvalue = &five; // declare pvalue as pointer to int and have it
// point to the location of five
*pvalue = 6; // Assign 6 into the location five.
```
下图显示了编译器如何分配内存。`值`需要内存存储指针,而`五`需要内存存储整数值 5:

###### 图 2A.19:指针变量的内存布局
当通过指针访问用户定义的类型时,还有第二个特殊运算符(-->)也用于对成员变量和函数进行解引用。在现代 C++ 中,这些指针被称为**原始指针**,它们的使用方式发生了显著变化。在 C 和 C++ 中使用指针对程序员来说一直是一个挑战,它们的不正确使用是许多问题的根源,最常见的是资源泄漏。资源泄漏是指程序获取了一个资源(内存、文件句柄或其他系统资源)供其使用,但在使用完毕后未能释放的情况。这些资源泄漏会导致性能问题、程序故障,甚至系统崩溃。在现代 C++ 中使用原始指针来管理资源的所有权现在已经被否决了,因为智能指针出现在 C++ 11 中。智能指针(在 STL 中实现为类)现在做了在您的主机系统中成为一个好公民所需的家务。更多相关内容将在*第三章*、*能与应之间的距离-对象、指针和继承*中介绍。
在前面的代码中,当`值`被声明时,编译器分配内存只存储它将要引用的内存的地址。像其他变量一样,您应该始终确保在使用指针之前对其进行初始化,因为取消对未初始化指针的引用会导致未定义的行为。究竟分配了多少内存来存储指针取决于编译器设计的系统和处理器支持的位数。但是所有指针的大小都是一样的,不管它们指向什么类型。
指针也可以传递给函数。这允许函数访问被指向的数据,并可能对其进行修改。考虑 swap 的以下实现:
```cpp
void swap(int* data1, int* data2)
{
int temp{*data1}; // Initialize temp from value pointed to by data1
*data1 = *data2; // Copy data pointed to by data2 into location
// pointed to by data1
*data2 = temp; // Store the temporarily cached value from temp
// into the location pointed to by data2
}
```
这展示了如何将指针声明为函数的参数,如何使用解引用操作符`*`从指针中获取值,以及如何通过解引用操作符设置值。
以下示例使用新运算符从主机系统分配内存,并使用删除运算符将其释放回主机系统:
```cpp
char* name = new char[20]; // Allocate 20 chars worth of memory and assign it
// to name.
Do something with name
delete [] name;
```
在前面的代码中,第一行使用新运算符的数组分配形式创建了一个 20 个字符的数组。它调用主机系统来分配 20 * sizeof(char)字节的内存供我们使用。具体分配多少内存由主机系统决定,但保证至少为 20 * sizeof(char)字节。如果它无法分配所需的内存,则会发生以下两种情况之一:
* 它将引发异常
* 它将返回`nullptr`。这是 C++ 11 中引入的一个特殊文字。早期,C++ 使用 0 或空值来表示无效指针。C++ 11 也使它成为强类型值。
在大多数系统中,第一个结果将是结果,您需要处理异常。第二种结果可能来自两种情况——调用 new 的 northrow 变体,即`new(STD::northrow)int[250]`,或者在异常处理开销没有足够确定性的嵌入式系统上。
最后,请注意,对 delete 的调用使用了 delete 运算符的数组形式,即带有方括号[]。确保新的和删除操作符使用相同的形式非常重要。当在用户定义的类型上使用 new 时(这将在下一节中讨论),它不仅仅是分配内存:
```cpp
MyClass* object = new MyClass;
```
在前面的代码中,对 new 的调用分配了足够的内存来存储 MyClass,如果成功,它将继续调用构造函数来初始化数据:
```cpp
MyClass* objects = new MyClass[12];
```
在前面的代码中,对 new 的调用分配了足够的内存来存储 MyClass 的 12 个副本,如果成功,它将继续调用构造函数 12 次来初始化每个对象的数据。
请注意,在前面的代码片段中声明的`对象`和`对象`具有相同类型的**。严格来说,`对象`应该是指向 MyClass 数组的指针,但实际上是指向 MyClass 实例的指针。`对象`指向 MyClass 数组中的第一个实例。**
**考虑以下代码摘录:
```cpp
void printMyClasses(MyClass* objects, size_t number)
{
for( auto i{0U} ; i(Option::Play);
```
### 练习 4:枚举-新旧学校
在本练习中,我们将实现一个程序,该程序使用枚举来表示预定义的值,并确定当它们被更改为限定范围的枚举时所需的相应更改。让我们开始吧:
1. 在 Eclipse 中打开**第 2 课**项目,然后在**项目浏览器**中,展开**第 2 课**,然后展开**练习 04** ,双击**练习 4.cpp** 在编辑器中打开本练习的文件。
2. 点击**启动配置**下拉菜单,选择**新启动配置……**。将**练习 4** 配置为使用名称**练习 4** 运行。
3. 完成后,它将是当前选定的启动配置。
4. Click on the **Run** button. Exercise 4 will run and produce the following output:

###### 图 2A.25:练习 4 输出
5. 在编辑器中检查代码。目前,我们可以比较苹果和橘子。在`printOrange()`的定义中,将参数更改为`Orange` :
```cpp
void printOrange(Orange orange)
```
6. Click on the **Run** button. When the Errors in Workspace dialog appears, click **Cancel**:

###### 图 2A.26:无法转换错误
通过改变参数类型,我们迫使编译器强制执行传递给函数的值的类型。
7. Call the `printOrange()` function twice by passing the `orange` `enum` variable in the initial call and the `apple` variable in the second call, respectively:
```cpp
printOrange(orange);
printOrange(apple);
```
这表明编译器正在隐式地将橙色和苹果转换成一个`int`,以便它可以调用该函数。另外,注意关于比较`苹果`和`橙`的警告。
8. 通过取一个 int 参数并将`orange` `枚举`的定义更改为以下值来恢复`printOrange()`功能:
```cpp
enum class Orange;
```
9. Click on the **Run** button. When the Errors in Workspace dialog appears, click **Cancel**:

###### 图 2A.27:作用域枚举更改的多个错误
10. Locate the first error listed for this build:

###### 图 2A.28:第一个作用域枚举错误
11. 关于作用域枚举,首先要注意的是,当引用枚举器时,它们必须有一个作用域说明符。因此,在编辑器中,转到并将这一行更改为以下内容:
```cpp
Orange orange{Orange::Hamlin};
```
12. Click on the **Run** button. When the Errors in Workspace dialog appears, click **Cancel**. Good news – the error count dropped to 8 from 9\. Check the errors in the console and locate the first one:

###### 图 2A.29:第二个作用域枚举错误
此错误报告无法找到插入运算符(<橙色类型。因为这涉及到一个基于模板的类(我们将在后面讨论),所以错误消息变得非常冗长。花一分钟时间查看从这个错误到下一个错误(红线)出现的所有消息。它向您展示了编译器试图做什么来编译那一行。
13. 将指示行改为如下:
```cpp
std::cout << "orange = " << static_cast(orange) << "\n";
```
14. Click on the **Run** button. When the Errors in Workspace dialog appears, click **Cancel**. Good news – the error count dropped to 6 from 8\. Check the errors in the console and locate the first one:

###### 图 2A.30:第三范围枚举错误
这个错误报告(最后)你不能比较苹果和橘子。在这一点上,我们认为程序试图做一些没有意义的事情,没有必要试图修复其余的事情。我们可以通过再次将它转换为 int 来修复这个错误,但是我们也需要为下一个错误进行转换。最后一个错误是巴伦西亚缺少`Orange::`范围说明符。
15. 留给你一个练习,让文件以`橙色`作为范围枚举再次编译。
在本练习中,我们发现范围枚举改进了 C++ 的强类型检查,如果我们希望将它们用作整数值,那么我们需要强制转换它们,这与隐式转换的非范围枚举不同。
#### 编译器错误疑难解答
从前面的练习中可以看出,编译器可以从一个错误中生成大量的错误和警告消息。这就是为什么建议先找到第一个错误并先修复它。在 IDEs 中开发或使用带有颜色代码错误的构建系统可以使这变得更容易。
### 结构和类别
枚举是用户定义类型中的第一种,但它们并没有真正扩展语言,以便我们能够在适当的抽象级别上表达问题的解决方案。然而,结构和类允许我们捕获和分组数据,然后关联方法以一致和有意义的方式操作数据。
如果我们考虑两个矩阵的乘法, *A (m x n)* 和 *B (n x p)* ,从而得到矩阵 *C (m x p)* ,那么 C 的第 I 行和第 jth 列的等式如下:

###### 图 2A.31:第 1 行和第 2 列的方程
如果我们每次想要乘两个矩阵时都要写它,我们最终会得到许多嵌套的 for 循环。但是如果我们可以把一个矩阵抽象成一个类,那么我们可以把它简单地表达为两个整数或两个浮点数的乘积:
```cpp
Matrix a;
Matrix b;
// Code to initialize the matrices
auto c = a * b;
```
这就是面向对象设计的美妙之处——数据封装和概念的抽象被解释得如此之深,以至于我们可以很容易地理解程序试图实现什么,而不会被细节所掩盖。一旦我们确定矩阵乘法被正确实现,那么我们就可以自由地专注于在更高的层次上解决我们的问题。
下面的讨论涉及到类,但它同样适用于结构,并且主要适用于联合。在我们学习如何定义和使用类之后,我们将概述类、结构和联合之间的区别。
### 分数等级
为了向您展示如何定义和使用类,我们将开发`分数`类来实现有理数。定义后,我们可以像使用任何其他内置类型(加、减、乘、除)一样使用`分数`,而不用担心细节——这是抽象。我们现在可以在更高的层次上思考和推理一个分数,也就是抽象的层次。
`分数`类将执行以下操作:
* 包含两个整数成员变量,`m _ 分子`和`m _ 分母`
* 提供复制自身、分配给自身、乘法、除法、加法和减法的方法
* 提供写入输出流的方法
为了实现上述目标,我们有以下定义:

###### 图 2A.32:操作的定义
此外,我们执行的操作将需要通过将其减少到最低项来标准化分数。为此,分子和分母都要除以它们的最大公约数(GCD)。
### 构造函数、初始化和析构函数
用 C++ 代码表示的类定义是用于在内存中创建对象和通过对象的方法操作对象的模式。我们需要做的第一件事是告诉编译器我们希望声明一个新的类型——类。要声明`分数`类,我们从以下内容开始:
```cpp
class Fraction
{
};
```
我们将它放在头文件 **Fraction.h** 中,因为我们希望在代码的其他区域重用这个类规范。
接下来我们需要做的是引入要存储在类中的数据,在这种情况下是`m _ 分子`和`m _ 分母`。它们都是 int 类型的:
```cpp
class Fraction
{
int m_numerator;
int m_denominator;
};
```
我们现在已经声明了要存储的数据,并给它们起了一个名字,熟悉数学的人都会理解每个成员变量存储了什么:

###### 图 2A.33:分数公式
由于这是一个类,默认情况下,任何声明的项目都被认为是`私有的`。这意味着没有外部实体可以访问这些变量。正是这种隐藏(使数据私有,就此而言,一些方法)的特性使得 C++ 中的封装成为可能。C++ 有三个类访问修饰符:
* **public** :这意味着成员(变量或函数)可以从类外的任何地方访问。
* **private** :这意味着不能从类外访问成员(变量或函数)。事实上,它甚至不能被查看。私有变量和函数只能从类内部或通过友元方法或类来访问。公共函数使用私有成员(变量和函数)来实现所需的功能。
* **受保护**:这是公私交叉。从类外部来看,变量或函数是私有的。但是,对于从声明受保护成员的类派生的任何类,它们都被视为公共的。
在我们对类的定义中,这一点不是很有用。让我们将声明更改为以下内容:
```cpp
class Fraction
{
public:
int m_numerator;
int m_denominator;
};
```
通过这样做,我们可以访问内部变量。`分数;`变量声明会导致编译器做两件事:
* 分配足够的内存来保存两个数据项(取决于类型,这可能涉及填充,也可能不涉及填充,即包含或添加未使用的内存来对齐成员以实现最有效的访问)。运算符的**size 可以告诉我们为我们的类分配了多少内存。**
* 通过调用**默认构造函数**初始化数据项。
这些步骤与编译器对内置类型所做的相同,也就是说,步骤 2 什么也不做,导致变量未初始化。但是这个默认构造函数是什么呢?它是做什么的?
首先,默认构造函数是一个特殊的成员函数。它是许多可能的构造函数之一,其中三个被认为是特殊成员函数。构造函数可以用零个、一个或多个参数来声明,就像任何其他函数一样,但是它们不指定返回类型。构造函数的特殊用途是初始化所有成员变量,以将对象置于定义良好的状态。如果成员变量本身是一个类,那么可能没有必要指定如何初始化变量。如果成员变量是内置类型,那么我们需要为它们提供初始值。
### 类特殊成员函数
当我们定义一个新类型(结构或类)时,编译器将为我们创建多达六(6)个特殊成员函数:
* **默认构造函数** ( `分数::分数()`):当没有提供参数时调用(如前一节)。这可以通过没有构造函数的参数列表或定义所有参数的默认值来实现,例如`分数(int 分子=0,分母=1)`。编译器提供了一个`隐式` `内联`默认构造函数来执行成员变量的默认初始化——对于内置类型,这意味着什么也不做。
* **析构函数** ( `分数::~分数()`):这是一个特殊的成员函数,在对象生命周期结束时调用。其目的是释放对象在其生存期内分配和保留的任何资源。编译器提供了一个`公共` `内联`成员函数,调用成员变量的析构函数。
* **复制构造函数** ( `分数::分数(const Fraction & )`):这是另一个构造函数,其中第一个参数是`分数&`的一种形式,没有其他参数,或者其余参数都有默认值。第一个参数的形式是`分数&`、`常量分数&`、`挥发性分数&`或`常量挥发性分数&`中的一种。我们稍后会处理`const`,但不会处理本书中的`volatile`。编译器提供了一个`非显式` `公共` `内联`成员函数,通常采用`Fraction::Fraction(const Fraction&)`的形式,按照初始化的顺序复制每个成员变量。
* **复制赋值** ( **分数&分数::运算符=(分数& )** ):这是一个名为**运算符=** 的成员函数,第一个参数是一个值或类的任何引用类型,在本例中为**分数**、**分数&** 、**常量分数&** 、**挥发分数&** 或**编译器提供了一个**公共** **内联**成员函数,通常采用**Fraction::Fraction(const Fraction&)**的形式,按照初始化的顺序复制每个成员变量。**
* **Move Constructor**(`Fraction::Fraction(Fraction&&)`):这是 C++ 11 中引入的一种新型构造函数,其中第一个参数是`Fraction & &`的一种形式,没有其他参数,或者其余参数都有默认值。第一个参数的形式是`分数& &`、`常量分数& &`、`挥发分& &`或`常量挥发分& &`中的一种。编译器提供了一个`非显式` `公共` `内联`成员函数,通常采用`Fraction::Fraction(Fraction&&)`的形式,按照初始化的顺序移动每个成员变量。
* **移动赋值** ( `分数&分数::运算符=(分数& & )`):这是 C++ 11 中引入的一种新型赋值运算符,是一个名为`运算符=`的成员函数,第一个参数是移动构造函数允许的任何形式。编译器提供一个`公共` `内联`成员函数,通常采用`Fraction::Fraction(Fraction&&)`的形式,按照初始化的顺序复制每个成员变量。
除了默认构造函数之外,这些函数处理管理这个类所拥有的资源——也就是说,如何复制/移动它们以及如何处置它们。另一方面,默认构造函数更像任何其他接受值的构造函数——它只初始化资源。
我们可以声明这些特殊函数中的任何一个,强制它们默认(也就是说,让编译器生成默认版本),或者强制它们不被创建。在其他特殊函数存在的情况下,也有关于何时自动生成这些函数的规则。前四个函数在概念上相对简单,但是两个“移动”特殊成员函数需要一些额外的解释。我们将在*第 3 章*、*能够和应该之间的距离——对象、指针和继承*中详细讨论所谓的移动语义,但目前它本质上是它所指示的——它将某物从一个对象移动到另一个对象。
### 隐式与显式构造函数
前面的描述讨论了编译器生成隐式或非显式构造函数。如果存在可以用一个参数调用的构造函数,例如复制构造函数或移动构造函数,默认情况下,允许编译器在必要时调用它,以便它可以将其从一种类型转换为另一种类型,从而允许对表达式、函数调用或赋值进行编码。这并不总是一个期望的行为,我们可能希望防止隐式转换,并确保如果我们类的用户真的想要转换,那么他们必须在程序中写出来。为此,我们在构造函数的声明前加上`显式的`关键字,如下所示:
```cpp
explicit Fraction(int numerator, int denominator = 1);
```
显式关键字也可以应用于其他运算符,编译器可以将其用于类型转换。
### 类特殊成员函数–编译器生成规则
首先,如果我们声明任何其他形式的构造函数——默认、复制、移动或用户定义,将不会生成`默认构造函数`。其他特殊成员函数都不会影响其生成。
其次,声明析构函数就不会产生`析构函数`。其他特殊成员函数都不会影响其生成。
其他四个特殊函数的生成取决于析构函数或其他特殊函数之一的声明,如下表所示:

###### 图 2A.34:特殊成员函数生成规则
### 默认和删除特殊成员功能
在 C++ 11 之前,如果我们想防止使用复制构造函数或复制赋值成员函数,那么我们必须将函数声明为私有的,并且不提供函数的定义:
```cpp
class Fraction
{
public:
Fraction();
private:
Fraction(const Fraction&);
Fraction& operator=(const Fraction&);
};
```
通过这种方式,我们确保了如果有人试图从类外部访问复制构造函数或复制赋值,那么编译器会生成一个错误,指出该函数不可访问。这仍然声明了函数,并且它们可以从类中访问。取消这些特殊的成员功能是一种有效的手段,但并不完美。
但是我们可以做得更好,因为 C++ 11 引入了两种新的声明形式,允许我们覆盖编译器的默认行为,如前面的规则中所定义的。
首先,我们可以通过用`= delete`后缀声明方法来强制编译器不生成方法,如下所示:
```cpp
Fraction(const Fraction&) = delete;
```
#### 注意
如果不使用参数,我们可以省略它的名称。任何函数或成员函数都是如此。事实上,根据为编译器设置的警告级别,它甚至可能会生成一个警告,指出没有使用该参数。
或者,我们可以使用`= default`后缀强制编译器生成其特殊成员函数的默认实现,如下所示:
```cpp
Fraction(const Fraction&) = default;
```
如果这只是函数的声明,那么我们也可以省略参数的名称。尽管如此,良好的实践要求我们应该命名参数以指示其用途。这样,我们类的用户就不需要查看调用函数的实现。
#### 注意
使用默认后缀声明一个特殊的成员函数被认为是用户定义的成员函数。
### 三/五法则和零法则
正如我们之前讨论的,除了默认构造函数之外,特殊成员函数处理管理这个类所拥有的资源的语义——即如何复制/移动它们以及如何处置它们。这导致了 C++ 社区中关于处理特殊函数的两条“规则”。
在 C++ 11 之前,有三的**规则,处理复制构造函数、复制赋值运算符和析构函数。它基本上声明我们需要实现这些方法中的一个,因为封装资源的管理并不简单。**
随着 C++ 11 中移动构造函数和移动赋值操作符的引入,这个规则扩展到了五的**规则。规则的本质没有改变。简单来说,特殊成员函数的数量增加到了五个。记住编译器生成的规则,还有一个额外的原因来确保所有五个特殊方法都被实现(或者强制 via = default),那就是,如果编译器没有访问移动语义函数的权限,它将尝试使用复制语义函数,而这可能不是所期望的。**
#### 注意
有关更多详细信息,请参见 C++ 核心指南的 C.ctor:构造函数、赋值函数和析构函数部分,可以在这里找到:[http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines](http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines)。
### 构造函数–初始化对象
构造函数的主要任务是将对象置于稳定状态,以便对象通过其成员函数执行的任何操作都会导致一致的定义行为。虽然前面的语句适用于复制和移动构造函数,但是它们通过不同的语义(从另一个对象复制或移动)来实现这一点。
有四种不同的机制可供我们控制一个对象的初始状态。在这种情况下,C++ 有很多使用初始化的规则。我们将不详细讨论 C++ 标准的默认初始化、零初始化、值初始化、常数初始化等等。只要知道最好的方法是明确你的变量的初始化。
**第一个**也是最不可取的初始化机制是给构造函数主体中的成员变量赋值,如下所示:
```cpp
Fraction::Fraction()
{
this->m_numerator = 0;
this->m_denominator = 1;
}
Fraction::Fraction(int numerator, int denominator)
{
m_numerator = numerator;
m_denominator = denominator;
}
```
很清楚使用什么值来初始化变量。严格来说,这不是类的初始化——按照标准,初始化是在构造函数的主体被调用时完成的。这很容易维护,尤其是在这个类中。对于具有多个构造函数和许多成员变量的大型类,这可能是一个维护问题。如果您更改一个构造函数,您将需要更改所有的构造函数。它还有一个问题,如果成员变量是引用类型(我们将在后面讨论),那么它不能在构造函数的主体中完成。
默认构造函数使用**这个**指针。每个成员函数,包括构造函数和析构函数,都用一个隐式参数调用(即使它从未声明过)–这个指针的**。**此**指向对象的当前实例。 **- >** 运算符是另一个去引用运算符,在本例中是简写,即 ***(this)。m _ 分子**。 **this- >** 的使用是可选的,可以省略。其他语言,如 Python,需要声明和使用隐式指针/引用(Python 中的约定是调用 *self* )。**
第二个**机制是成员初始化列表的使用,它的使用有一个警告。对于我们的分数类,我们有以下内容:**
```cpp
Fraction::Fraction() : m_numerator(0), m_denominator(1)
{
}
Fraction::Fraction(int numerator, int denominator) :
m_numerator(numerator), m_denominator(denominator)
{
}
```
冒号后,、左大括号前的代码段{、in(`m _ 分子(0)、m _ 分母(1)`和`m _ 分子(分子)、m _ 分母(分母)`是成员初始化列表。我们可以在成员初始化列表中初始化一个引用类型。
#### 成员初始化列表顺序
无论成员在成员初始化列表中的放置顺序如何,编译器都将按照它们在类中声明的顺序初始化成员。
第三个****推荐的**初始化是 C++ 11 中引入的默认成员初始化。当使用赋值或括号初始值设定项声明变量时,我们定义默认初始值:**
```cpp
class Fraction
{
public:
int m_numerator = 0; // equals initializer
int m_denominator{1}; // brace initializer
};
```
如果构造函数没有定义成员变量的初始值,那么这个默认值将用于初始化变量。这样做的好处是确保所有的构造函数产生相同的初始化,除非它们在构造函数的定义中被显式修改。
C++ 11 还引入了第四种初始化风格,称为构造函数委托。它是对成员初始化列表的修改,在该列表中,不列出成员变量及其初始值,而是调用另一个构造函数。以下示例是人为设计的,您不会以这种方式编写类,但它显示了构造函数委托的语法:
```cpp
Fraction::Fraction(int numerator) : m_numerator(numerator), m_denominator(1)
{
}
Fraction::Fraction(int numerator, int denominator) : Fraction(numerator)
{
auto factor = std::gcd(numerator, denominator);
m_numerator /= factor;
m_denominator = denominator / factor;
}
```
从具有两个参数的构造函数中调用单参数构造函数。
### 练习 5:声明和初始化分数
在本练习中,我们将使用不同的可用技术实现类成员初始化,包括构造函数委托。让我们开始吧:
1. 在 Eclipse 中打开**第 2 课**项目,然后在**项目浏览器**中,展开**第 2 课**,然后展开**练习 05** ,双击**练习 5.cpp** 在编辑器中打开本练习的文件。
2. 点击**启动配置**下拉菜单,选择**新启动配置……**。将**练习 5** 配置为以练习 5 的名称运行。
3. 完成后,它将是当前选定的启动配置。
4. Click on the **Run** button. **Exercise 5** will run and produce something similar to the following output:

###### 图 2A.35:练习 5 典型输出
为分数报告的值来自不以任何方式初始化成员变量。如果你再运行一次,你很可能会得到不同的分数。
5. 点击几次**运行**按钮。你会看到分数发生了变化。
6. 在编辑器中,将构造函数更改如下:
```cpp
Fraction() : m_numerator{0}, m_denominator{1}
{
}
```
7. Click on the **Run** button and observe the output:

###### 图 2A.36:修改后的练习 5 输出
这一次,分数值由我们在成员初始化列表中指定的值定义。
8. 在编辑器中,添加以下两个`构造函数` :
```cpp
Fraction(int numerator) : m_numerator(numerator), m_denominator(1)
{
}
Fraction(int numerator, int denominator) : Fraction(numerator)
{
auto factor = std::gcd(numerator, denominator);
m_numerator /= factor;
m_denominator = denominator / factor;
}
```
9. 在主功能中,将`分数`的声明改为包括初始化:
```cpp
Fraction fraction{3,2};
```
10. Click on the **Run** button and observe the output:

###### 图 2A.37:构造函数委托的例子
在本练习中,我们使用成员初始化列表和构造函数委托实现了成员变量初始化。*我们将返回到练习 7“向分数类添加运算符”中的分数。*
### 值与参考值和常量
到目前为止,我们只处理了值类型,即变量保存对象的值。指针保存我们感兴趣的值(即对象的地址)。但这会导致效率低下和资源管理问题。我们将在这里讨论如何解决效率低下的问题,但在*第 3 章*、*可以和应该之间的距离—对象、指针和继承*中讨论资源管理问题。
考虑以下问题..我们有一个 10×10 的双类型矩阵,我们希望为它编写一个求逆函数。该类声明如下:
```cpp
class Matrix10x10
{
private:
double m_data[10][10];
};
```
如果我们取`sizeof(matrix x10x 10)`,我们会得到`sizeof(double)`x10x 10 = 800 字节。现在,如果我们为此实现一个逆矩阵函数,它的签名可能如下所示:
```cpp
Matrix10x10 invert(Matrix10x10 lhs);
Matrix10x10 mat;
// set up mat
Matrix10x10 inv = invert(mat);
```
首先,这意味着编译器需要将`mat`保存的值传递给`invert()`函数,并将 800 字节复制到堆栈上。然后,该函数做它需要做的任何事情来反转矩阵(一个 L-U 分解,行列式的计算——无论实现者选择什么方法),然后将 800 字节的结果复制回`inv`变量。在堆栈上传递大值从来都不是一个好主意,原因有二:
* 堆栈是主机操作系统给我们程序的有限资源。
* 在系统中复制大值是低效的。
这种方法被称为按值传递。也就是说,我们希望处理的项目的值被复制到函数中。
在 C(和 C++)中,这个限制是通过使用指针来解决的。前面的代码可能变成下面的代码:
```cpp
void invert(Matrix10x10* src, Matrix10x10* inv);
Matrix10x10 mat;
Matrix10x10 inv;
// set up mat
invert(&mat, &inv);
```
这里,我们只是将 src 的地址和反向结果的目标作为两个指针传递(这是少量字节)。不幸的是,这导致每次我们使用`src`或`inv`时,函数内部的代码都必须使用取消引用运算符(`*`),使得代码更难阅读。此外,指针的使用导致了许多问题。
C++ 引入了一种更好的方法——变量别名或引用。引用类型用&符号( **&** )运算符声明。因此,我们可以如下声明反转方法:
```cpp
void invert(Matrix10x10& src, Matrix10x10& inv);
Matrix10x10 mat;
Matrix10x10 inv;
// set up mat
invert(mat, inv);
```
请注意,调用方法不需要特殊运算符来传递引用。从编译器的角度来看,引用仍然是有一个限制的指针——它不能保存 nullptr。从程序员的角度来看,引用允许我们对代码进行推理,而不必担心在正确的地方有正确的取消引用操作符。这就是所谓的**通过参考**。
我们看到引用被传递给复制构造函数和复制赋值方法。引用的类型,当用于它们的移动等价物时,被称为**右值引用操作符**,将在*第 3 章*、*Can 和 short 之间的距离-对象、指针和继承*中解释。
`传递值`的一个优点是,我们不会无意中修改传递到方法中的变量值。现在,如果我们通过引用传递,我们不能再保证我们正在调用的方法不会修改原始变量。为了解决这个问题,我们可以将 invert 方法的签名更改如下:
```cpp
void invert(const Matrix10x10& src, Matrix10x10& inv);
```
const 关键字告诉编译器,在处理`invert()`函数的定义时,给`src`引用的值的任何部分赋值都是非法的。如果该方法试图修改 src,编译器将生成一个错误。
在指定类型–变量部分,我们发现`汽车标题`的声明导致`标题`属于`常量字符*`类型。现在,我们可以解释`const`部分。
`标题`变量是**一个指向常量**的指针。换句话说,我们不能改变存储在我们所指向的内存中的数据的值。因此,我们无法做到以下几点:
```cpp
*title = 's';
```
这是因为编译器会生成与更改常数值相关的错误。然而,我们可以改变存储在指针中的值。我们可以执行以下操作:
```cpp
title = "Maid Marian";
```
我们现在已经介绍了用作函数参数类型的引用,但是它们也可以用作成员变量而不是指针。引用和指针之间有区别:
引用必须引用实际的对象(没有 nullptr 的等价物)。引用一旦初始化就不能更改(这导致引用必须是初始化的默认成员或出现在成员初始化列表中)。只要对该对象的引用存在,该对象就必须存在(如果该对象可以在引用被销毁之前被销毁,那么如果试图访问该对象,就有可能出现未定义的行为)。
### 练习 6:声明和使用引用类型
在本练习中,我们将声明并使用引用类型,以使代码高效且易于阅读。让我们开始吧:
1. 在 Eclipse 中打开**第 2 课**项目,然后在**项目浏览器**中,展开**第 2 课**,然后展开**练习 06** ,双击**练习 6.cpp** 在编辑器中打开本练习的文件。
2. 点击**启动配置**下拉菜单,选择**新启动配置……**。将**练习 6** 配置为以练习 6 的名称运行。
3. 完成后,它将是当前选定的启动配置。
4. Click on the **Run** button. Exercise 6 will run and produce something similar to the following output:

###### 图 2A.38:练习 6 的输出
通过检查代码并将其与输出进行比较,我们会发现`值`变量允许我们操作(读取和写入)存储在`值`变量中的数据。我们有一个参考,`值`到`值`变量。我们还可以看到存储在`a`和`b`变量中的值是通过`swap()`函数交换的。
5. 在编辑器中,更改 swap 的函数定义:
```cpp
void swap(const int& lhs, const int& rhs)
```
6. 点击**运行**按钮。出现“工作空间中的错误”对话框时,单击**取消**。编译器报告的第一个错误如下所示:

###### 图 2A.39:赋值时的只读错误
通过将参数从`int & lhs`更改为`const int & lhs`,我们已经告诉编译器在这个函数中不应该更改参数。因为我们在函数中给 lhs 赋值,编译器会生成 lhs 为只读的错误并终止。
### 执行标准操作符
要像使用内置类一样使用分数,我们需要它们与标准数学运算符(`+、-、*、/`)及其赋值对应物(`+=、-=、*=、/=`)一起工作。如果您不熟悉赋值运算符,请考虑以下两个表达式——它们产生相同的输出:
```cpp
a = a + b;
a += b;
```
为 Fraction 声明这两个运算符的语法如下:
```cpp
// member function declarations
Fraction& operator+=(const Fraction& rhs);
Fraction operator+(const Fraction& rhs) const;
// normal function declaration of operator+
Fraction operator+(const Fraction& lhs, const Fraction& rhs);
```
因为`运算符+=`方法修改左侧变量的内容(将 a 添加到 b,然后再次存储在 a 中),所以建议将其实现为成员变量。在这种情况下,由于我们没有创建新的值,我们可以返回对现有 lhs 的引用。
另一方面,operator+方法不应该修改 lhs 或 rhs 并返回一个新对象。实现者可以自由地将其实现为成员函数或自由函数。两者都显示在前面的代码中,但应该只存在一个。关于成员函数的实现,有趣的是声明末尾的 const 关键字。这告诉编译器,当调用这个成员函数时,它不会修改对象的内部状态。虽然这两种方法都有效,但如果可能的话,`运算符+`应该作为类外的正常函数来实现。
其他运算符`–(减)`、`*(乘)`和`/(除)`也可以使用相同的方法。前面的方法实现了标准数学运算符的语义,并使我们的类型像内置类型一样工作。
### 实现输出流操作符(< <)
C++ 将输入/输出(I/O)抽象到标准库中的流类层次结构中(我们将在*章 2B* 、*不允许鸭子-模板和演绎*中讨论)。在*练习 5* 、*声明和初始化分数*中,我们看到可以将分数插入输出流,如下所示:
```cpp
std::cout << "fraction = " << fraction.getNumerator() << "/"
<< fraction.getDenominator() << "\n";
```
到目前为止,对于我们的 Fraction 类,我们已经通过使用`getmoleculator()`和`get 分母()`方法从外部访问数据值写出了分子和分母值,但是还有更好的方法。作为让我们的类在 C++ 中成为一流公民的一部分,在这种情况下,我们应该重载输入/输出操作符。在本章中,我们将只看输出操作符,< <,也称为插入操作符。这样,我们可以用一个更干净的版本来替换以前的代码:
```cpp
std::cout << "fraction = " << fraction << "\n";
```
我们可以将运算符重载为友元函数或普通函数(如果类提供了我们需要插入的数据的 getter 函数)。出于我们的目的,我们将其定义为一个普通函数:
```cpp
inline std::ostream& operator<< (std::ostream &out, const Fraction &rhs)
{
out << rhs.getNumerator() << " / " << rhs.getDenominator();
return out;
}
```
### 构建我们的代码
在我们深入研究实现操作符并将我们的 Fraction 转换为 C++ 世界中成熟类型的练习之前,我们需要简单讨论一下我们将类的各个部分放在哪里——声明和定义。声明是我们类的蓝图,指出它需要什么样的数据存储以及它将实现的方法。定义是每个方法的实际实现细节。
在像 Java 和 C#这样的语言中,声明和定义是相同的,它们必须存在于一个文件中(Java)或者跨多个文件(C#分部类)。在 C++ 中,根据类和您希望向其他类公开的程度,声明必须出现在头文件中(可以是其他文件中使用的 **#included** ),定义可以出现在三个位置之一——内嵌在定义中、**内嵌在与定义相同的文件中或单独的实现文件中。**
头文件通常用。hpp 扩展名,而实现文件通常是`*。cpp`或`*。cxx`。实施文件也称为**翻译单元**。通过将一个函数定义为内联函数,我们允许编译器以一种甚至可能不存在于最终程序中的方式优化代码——它将我们放入函数的步骤替换为我们调用函数的位置。
### 练习 7:向分数类添加运算符
在本练习中,我们旨在使用单元测试来开发功能,从而在我们的 Fraction 类中实现运算符。这使得我们的分数类成为一个真正的类型。让我们开始吧:
1. 在 Eclipse 中打开**第 2 课**项目,然后在**项目浏览器**中,展开**第 2 课**,然后展开**练习 07** ,双击**练习 7.cpp** 在编辑器中打开本练习的文件。
2. 点击**启动配置**下拉菜单,选择**新启动配置……**。将练习 7 配置为以练习 7 的名称运行。
3. 完成后,它将是当前选定的启动配置。
4. 我们还需要配置一个单元测试。在 Eclipse 中,点击名为**运行** | **运行配置…** 的菜单项,右键点击左侧 **C/C++ 单元**,选择**新配置**。
5. 将名称从`第 2A 课调试`改为`练习 7 测试`。
6. 在 **C/C++ 应用**下,选择**搜索项目**选项,并在新对话框中选择**测试**。
7. Next, go to the **C/C++ Testing** tab and select **Google Tests Runner** in the dropdown. Click on **Apply** at the bottom of the dialog and click on the **Run** option for the test, which we have to run for the first time:

###### 图 2A.40:测试失败-乘法
8. 在编辑器中打开 **Fraction.cpp** 文件,找到`运算符*=`函数。用以下代码更新:
```cpp
Fraction& Fraction::operator*=(const Fraction& rhs)
{
Fraction tmp(m_numerator*rhs.m_numerator, m_denominator*rhs.m_denominator);
*this = tmp;
return *this;
}
```
9. Click on the **Run** button to rerun the tests. This time, all the tests pass:

###### 图 2A.41:通过测试
10. 在您的 IDE 中,打开**测试/分数测试. cpp** 文件,找到失败的两个测试。一个测试了**操作员*=()** ,另一个测试了**操作员*(T5】。固定**操作符*=()** 如何固定**操作符*()** ?如果你在编辑器中打开 Fraction.hpp,你会发现**运算符*(T11)**函数是通过调用**运算符*=()** 为你实现的,也就是说,它被标记为内联的,是一个普通函数,而不是成员函数。一般来说,这是重载这些运算符时要采取的方法——修改调用它的对象的方法是成员函数,而必须生成新值的方法是调用成员函数的普通函数。**
11. 在编辑器中打开 **Fraction.hpp** 并更改文件顶部附近的行,使其内容如下:
```cpp
#define EXERCISE7_STEP 11
```
12. Click on the **Run** button to rerun the tests – this time, we have added two more tests that fail – `AddFractions` and `AddFractions2`:

###### 图 2A.42:附加测试失败
13. 在**函数. cpp** 文件中找到`运算符+=`函数。
14. 对功能进行必要的更改,点击**运行**按钮重新运行测试,直到测试通过。看看前面给出的定义其操作的等式,看看`算子*=()`是如何实现的。
15. 在编辑器中打开 **Fraction.hpp** ,将文件顶部附近的行改为这样:
```cpp
#define EXERCISE7_STEP 15
```
16. 点击**运行**按钮重新运行测试——这次,我们又增加了两个失败的测试——`减法分数`和`减法分数 2`。
17. 在 Function.cpp 文件中找到`运算符-=`函数。
18. 对功能进行必要的更改,点击**运行**按钮重新运行测试,直到测试通过。
19. 在编辑器中打开 **Fraction.hpp** ,将文件顶部附近的行改为这样:
```cpp
#define EXERCISE7_STEP 19
```
20. 点击**运行**按钮重新运行测试–这一次,我们又增加了两个失败的测试–**分流**和**分流 2** 。
21. 在**函数. cpp** 文件中找到`运算符/=`函数。
22. 对功能进行必要的更改,点击**运行**按钮重新运行测试,直到测试通过。
23. 在编辑器中打开 **Fraction.hpp** ,将文件顶部附近的行改为这样:
```cpp
#define EXERCISE7_STEP 23
```
24. 点击**运行**按钮重新运行测试–这一次,我们又增加了一个失败的测试–`插入操作员`。
25. 在 Function.hpp 文件中找到`运算符< <`函数。
26. 对功能进行必要的更改,点击**运行**按钮重新运行测试,直到测试通过。
27. 从**启动配置**中,选择**练习 7** 并点击**运行**按钮。这将产生以下输出:

###### 图 2A.43:函数分数类
这就完成了我们现在对`分数`类的实现。当我们在*第三章*、*可以和应该之间的距离——对象、指针和继承*中考虑异常时,我们将再次回到它,这样我们就可以处理分数中的非法值(分母为 0)。
### 功能过载
C++ 支持一种称为函数重载的特性,即两个或多个函数具有相同的名称,但是它们的参数列表不同。参数的数量可以相同,但至少有一种参数类型必须不同。或者,它们可能有不同数量的参数。所以,多种功能的功能原型是不同的。但是,两个函数不能具有相同的函数名、相同的参数类型和不同的返回类型。以下是重载的一个示例:
```cpp
std::ostream& print(std::ostream& os, int value) {
os << value << " is an int\n";
return os;
}
std::ostream& print(std::ostream& os, float value) {
os << value << " is a single precision float\n";
return os;
}
std::ostream& print(std::ostream& os, double value) {
os << value << " is a double precision float \n";
return os;
}
// The next function causes the compiler to generate an error
// as it only differs by return type.
void print(std::ostream& os, double value) {
os << value << " is a double precision float!\n";
}
```
到目前为止,`分式`上的多个构造函数和重载的算术运算符都是重载函数的例子,编译器在遇到这些函数中的一个时必须引用它们。考虑以下代码:
```cpp
int main(int argc, char** argv) {
print(42);
}
```
当编译器遇到行`print(42)`时,它需要计算出调用哪个先前定义的函数,因此它执行以下过程(非常简化):

###### 图 2A.44:功能过载分辨率(简化)
C++ 标准定义了编译器如何根据如何操作(即转换)参数以获得匹配来确定最佳候选函数的规则。如果不需要转换,那么该函数是最佳匹配。
### 类、结构和联合
当您定义一个类并且没有指定访问修饰符(公共、受保护、私有)时,默认情况下所有成员都是私有的:
```cpp
class Fraction
{
Fraction() {}; // All of these are private
int m_numerator;
int m_denominator;
};
```
当您定义结构且未指定访问修饰符(公共、受保护、私有)时,默认情况下,所有成员都是公共的:
```cpp
struct Fraction
{
Fraction() {}; // All of these are public
int m_numerator;
int m_denominator;
};
```
还有一个区别,我们将在解释继承和多态性后再看。联合是一种不同于结构和类的数据构造,但却是相同的。联合是一种特殊类型的结构声明,其中所有成员占用相同的内存,并且在给定时间只有一个成员有效。`联盟`声明的示例如下:
```cpp
union variant
{
int m_ivalue;
float m_fvalue;
double m_dvalue;
};
```
当您定义联合并且没有指定访问修饰符(公共、受保护、私有)时,默认情况下所有成员都是公共的。
联合的主要问题是,没有内在的方法知道在任何给定的时间哪个值是有效的。这是通过定义一个被称为*标记的联合*来解决的——也就是说,一个保存联合的结构和一个标识它是否是有效值的枚举。对于联合中可以包含什么和不能包含什么,还有其他限制(例如,只有一个成员可以有默认的成员初始值设定项)。我们不会在这本书里深入探讨工会。
### 活动 1:图形处理
在现代计算环境中,矩阵无处不在地被用来解决各种问题——解联立方程、分析电网或电路、对图形渲染对象进行操作,以及实现机器学习。在图形世界中,无论是二维(2D)还是三维(3D),您想要对对象执行的所有操作都可以在矩阵乘法的帮助下完成。您的团队负责开发点的表示、变换矩阵以及您可能想要对它们执行的操作。按照以下步骤实现:
1. 从**第 2A 课/练习 01** 文件夹加载准备好的项目。
2. 创建一个名为 **Point3d** 的类,它可以默认构造为原点,或者使用一个由三个或四个值组成的初始化列表(数据直接存储在类中)。
3. 创建一个名为 **Matrix3d** 的类,它可以默认构造为一个身份矩阵,或者使用嵌套的初始化列表来提供所有的值(数据直接存储在类中)。
4. 在**点 3d** 上,重载`运算符()`,使其接受(`索引`)参数,以便返回位于`x(0)`、`y(1)`、`z(2)`和`w(3)`的值。
5. 在**矩阵 3d** 上,重载`运算符()`以获取(`行,col`列)参数,使其返回值。
6. 添加单元测试来验证上述所有特性。
7. 将`运算符*=(const Matrix3d & )`和`运算符==(const Matrix3d & )`添加到 **Matrix3d** 类中,并对它们进行单元测试。
8. 添加将两个**矩阵 3d** 对象和一个**矩阵 3d** 对象乘以一个**点 3d** 对象的自由函数。
9. 添加创建矩阵的独立方法,以平移、缩放和旋转(围绕 x、y、z 轴)及其单元测试。
执行上述步骤后,预期输出如下:

###### 图 2A.45:成功运行活动程序
就本次活动而言,我们不会担心指数超出范围的可能性。我们将在*第 3 章*、*能够和应该之间的距离——对象、指针和继承*中讨论这一点。单位矩阵是一个正方形矩阵(在我们的例子中是 4 x 4),其中对角线上的所有值都设置为 1,其他所有值都为零。
当使用三维图形时,我们为点(顶点)和变换使用增广矩阵,以便所有的变换(平移、缩放、旋转)都可以通过使用乘法来实现。
一个`n × m`矩阵是一个由 n 行 m 个数组成的数组。例如,一个`2 x 3`矩阵可能如下所示:

###### 图 2A . 46:2x 3 的矩阵
一个三维顶点可以表示为一个`三元组(x,y,z)`。然而,我们用另一个纵坐标`w (=1 代表一个顶点,=0 代表一个方向)`来扩充它,使它成为一个`四元组(x,y,z,1)`。我们不使用元组,而是将其放在`4 x 1`矩阵中,如下所示:

###### 图 2A.47: 4x1 矩阵
如果我们将`4×1`矩阵(点)乘以一个`4×4`矩阵(变换),我们就可以操纵该点。如果`Ti`代表一个变换,那么我们可以将这些变换相乘来实现对点的一些操作:

###### 图 2A.48:乘法变换
要乘以一个变换矩阵,`A x P = B`,我们执行以下操作:

###### 图 2A.49:乘法变换矩阵
我们也可以这样表达:

###### 图 2A.50:乘法变换的表达式
同样,两个`4×4`矩阵相乘也可以得到相同的结果,`AxB=C`:

###### 图 2A . 51 4x 4 矩阵乘法的表达式:
转换的矩阵如下:

###### 图 2A.52:用于转换的矩阵列表
#### 注意
这项活动的解决方案可以在第 635 页找到。
## 总结
在这一章中,我们学习了 C++ 中的类型。首先,我们接触了内置类型,然后学习了如何创建我们自己的行为类似于内置类型的类型。我们学习了如何声明和初始化变量,了解了编译器从源代码中生成什么,将变量放在哪里,链接器如何将变量放在一起,以及在计算机内存中是什么样子。我们学习了一些围绕零规则和五规则的 C++ 部落智慧。这些构成了 C++ 的构建模块。在下一章中,我们将研究用 C++ 模板创建函数和类,并进一步探索应用于模板的类型推导。********
================================================
FILE: docs/adv-cpp/03.md
================================================
# 三、不允许鸭子——模板和推导(二)
## 学习目标
本章结束时,您将能够:
* 使用继承和多态性开发自己的类,以获得更大的效果
* 实现一个别名,使您的代码更容易阅读
* 使用 SFINAE 和 constexpr 开发模板来简化代码
* 使用 STL 实现您自己的解决方案,以利用泛型编程
* 描述类型演绎的背景和基本规则
本章将向您展示如何通过继承、多态性和模板来定义和扩展您的类型。
## 简介
在前一章中,我们学习了如何在单元测试的帮助下开发我们自己的类型(类),并使它们像内置类型一样工作。我们被介绍了函数重载、三/五法则和零法则。
在本章中,我们将学习如何进一步扩展类型系统。我们将学习如何使用模板创建函数和类,并重新访问函数重载,因为它受到模板使用的影响。我们将被介绍一项新技术 **SFINAE** ,并使用它来控制我们模板中包含在生成代码中的部分。
## 继承、多态性和接口
到目前为止,在我们面向对象设计和 C++ 的旅程中,我们一直专注于抽象和数据封装。我们现在将注意力转向**遗传**和**多态性**。什么是继承?什么是多态性?我们为什么需要它?考虑以下三个对象:

###### 图 2B.1:车辆物体
在上图中,我们可以看到有三个非常不同的对象。他们有一些共同点。它们都有轮子(不同的数量)、发动机(不同的尺寸、功率或配置)、启动发动机、驱动、踩刹车、停止发动机等等,使用它们我们可以做一些事情。
因此,我们可以把它们抽象成一种叫做载体的东西,来展示这些属性和一般行为。如果我们将其表示为 C++ 类,它可能如下所示:
```cpp
class Vehicle
{
public:
Vehicle() = default;
Vehicle(int numberWheels, int engineSize) :
m_numberOfWheels{numberWheels}, m_engineSizeCC{engineSize}
{
}
bool StartEngine()
{
std::cout << "Vehicle::StartEngine " << m_engineSizeCC << " CC\n";
return true;
};
void Drive()
{
std::cout << "Vehicle::Drive\n";
};
void ApplyBrakes()
{
std::cout << "Vehicle::ApplyBrakes to " << m_numberOfWheels << " wheels\n";
};
bool StopEngine()
{
std::cout << "Vehicle::StopEngine\n";
return true;
};
private:
int m_numberOfWheels {4};
int m_engineSizeCC{1000};
};
```
`车辆`类是`摩托车`、`汽车`、`卡车`的更广义(或抽象)的表达。我们现在可以通过重用车辆类中已经可用的东西来创建更专门的类型。我们将通过使用继承来重用 Vehicle 的属性和方法。继承的语法如下:
```cpp
class DerivedClassName : access_modifier BaseClassName
{
// Body of DerivedClass
};
```
我们之前遇到过`公共`、`受保护`和`私有`等访问修饰符。它们控制我们如何访问基类的成员。摩托车等级将按如下方式推导:
```cpp
class Motorcycle : public Vehicle
{
public:
Motorcycle(int engineSize) : Vehicle(2, engineSize) {};
};
```
在这种情况下,车辆类被称为**基类**或**超类**,而摩托车类被称为**衍生类**或**子类**。我们可以用图形表示如下,箭头从派生类指向基类:

###### 图 2B.2:车辆等级体系
但是摩托车的驾驶方式不同于普通车辆。因此,我们需要修改`摩托车`类,使其行为不同。更新后的代码如下:
```cpp
class Motorcycle : public Vehicle
{
public:
Motorcycle(int engineSize) : Vehicle(2, engineSize) {};
void Drive()
{
std::cout << "Motorcycle::Drive\n";
};
};
```
如果我们考虑面向对象的设计,这是关于用协作的对象来建模问题空间。这些对象通过消息相互通信。现在,我们有两个类以不同的方式响应相同的消息(驱动方法)。消息的发送者不知道会发生什么,也不真正关心,这就是多态的本质。
#### 注意
多态来自希腊语 poly 和 morph,其中`poly`表示多,`morph`表示形式。所以,多态性意味着`有多种形式`。
我们现在可以使用这些类来测试多态性:
```cpp
#include
int main()
{
Vehicle vehicle;
Motorcycle cycle{1500};
Vehicle* myVehicle{&vehicle};
myVehicle->StartEngine();
myVehicle->Drive();
myVehicle->ApplyBrakes();
myVehicle->StopEngine();
myVehicle = &cycle;
myVehicle->StartEngine();
myVehicle->Drive();
myVehicle->ApplyBrakes();
myVehicle->StopEngine();
return 0;
}
```
如果我们编译并运行这个程序,我们会得到以下输出:

###### 图 2B.3:车辆程序输出
上图截图中`车辆::StartEngine 1500 cc`后的行都与`摩托车`有关。但是驱动线仍然显示`车辆::驱动`而不是预期的`摩托车::驱动`。这是怎么回事?问题是我们没有告诉编译器`Vehicle`类中的`Drive`方法可以被派生类修改(或者覆盖)。我们需要在代码中做一个改变:
```cpp
virtual void Drive()
{
std::cout << "Vehicle::Drive\n";
};
```
通过在成员函数声明前添加`virtual`关键字,我们告诉编译器,派生类可以(但不必)重写或替换该函数。如果我们进行这种更改,然后编译并运行程序,我们会得到以下输出:

###### 图 2B.4:使用虚拟方法的车辆程序输出
现在,我们已经了解了遗传和多态性。我们使用指向`车辆`类的指针来控制`摩托车`类。作为最佳实践,应该对代码进行另一次更改。我们还应该将`摩托车`中`驱动`功能的声明更改如下:
```cpp
void Drive() override
{
std::cout << "Motorcycle::Drive\n";
};
```
C++ 11 引入了`override`关键字作为对编译器的提示,声明一个特定的方法应该具有与其父树中某处的方法相同的函数原型。如果找不到,编译器将报告错误。这是一个非常有用的特性,可以节省您几个小时的调试时间。如果编译器有办法报告错误,就使用它。越早发现缺陷,越容易修复。最后一个变化是,每当我们向一个类添加一个虚函数时,我们必须声明它的析构函数`为虚函数`:
```cpp
class Vehicle
{
public:
// Constructors - hidden
virtual ~Vehicle() = default; // Virtual Destructor
// Other methods and data -- hidden
};
```
在虚拟化之前,我们通过`Drive()`功能看到了这一点。当通过指向车辆的指针调用析构函数时,它需要知道要调用哪个析构函数。因此,虚拟化可以实现这一点。如果做不到这一点,那么最终可能会出现资源泄漏或拼接对象。
### 继承和访问说明符
正如我们前面提到的,从超类继承一个子类的一般形式如下:
```cpp
class DerivedClassName : access_modifier BaseClassName
```
当我们从车辆类派生摩托车类时,我们使用以下代码:
```cpp
class Motorcycle : public Vehicle
```
访问修饰符是可选的,也是我们之前遇到过的修饰符之一:`公共`、`受保护`、`私有`。在下表中,您可以看到基类成员的可访问性。如果省略了 access_modifier,则编译器会假定指定了 private。

###### 图 2B.5:派生类中基类成员的可访问性
### 抽象类和接口
到目前为止,我们讨论的所有类都是**具体类**——它们可以被实例化为一个变量的类型。还有另一种类型的类——一个**抽象类**,它包含至少一个**纯虚拟成员函数**。纯虚函数是类中没有定义(或实现)的虚函数。并且因为它没有实现,所以类的格式不正确(或者是抽象的),不能被实例化。如果你试图创建一个抽象类型的变量,那么编译器会产生一个错误。
要声明纯虚拟成员函数,以`= 0`结束函数原型声明。为了使 Drive()成为 Vehicle 类中的纯虚拟函数,我们将声明如下:
```cpp
virtual void Drive() = 0;
```
现在,为了能够使用派生类作为变量类型(例如`摩托车`类),它必须定义`驱动()`函数的实现。
但是,您可以将变量声明为抽象类的指针或抽象类的引用。无论是哪种情况,它都必须指向或引用从抽象类派生的某个非抽象类。
在 Java 中,有一个关键字接口,允许你定义一个全是纯虚函数的类。在 C++ 中,通过声明一个只声明公共纯虚函数的类(和一个虚拟析构函数),可以实现同样的效果。这样,我们定义了一个接口。
#### 注意
在解决本章中的任何实际问题之前,下载本书的 GitHub 资源库([https://github.com/TrainingByPackt/Advanced-CPlusPlus](https://github.com/TrainingByPackt/Advanced-CPlusPlus))并导入 Eclipse 中 2B 课的文件夹,以便您可以查看每个练习和活动的代码。
### 练习 1:用多态性实现游戏角色
在本练习中,我们将演示继承、接口和多态性。我们将从角色扮演游戏的特别实现开始,并将其发展成更通用和可扩展的。让我们开始吧:
1. 打开 Eclipse,使用在**第 2B 章**示例文件夹中找到的文件创建一个名为**第 2B 章**的新项目。
2. 由于这是一个基于 **CMake 的项目**,将当前的构建器改为 **Cmake Build(可移植)**。
3. 转到**项目** | **构建所有**菜单构建所有练习。默认情况下,屏幕底部的控制台会显示 **CMake 控制台【第 2B 课】**。
4. Configure a **New Launch Configuration** named **L2BExercise1** that runs the **Exercise1** binary and click on **Run** to build and run **Exercise 1**. You will receive the following output:

###### 图 2B.6:练习 1 默认输出
5. Open **Exercise1.cpp** in the editor and examine the existing code. You will notice that the three characters are implemented as separate classes and that they are each instantiated and manipulated separately by calling `speak()` and `act()` directly. This is fine for a small program. But as the game grew to tens or hundreds of characters, it would become unmanageable. So, we need to abstract all the characters. Add the following Interface declaration to the top of the file:
```cpp
class ICharacter
{
public:
~ICharacter() {
std::cout << "Destroying Character\n";
}
virtual void speak() = 0;
virtual void act() = 0;
};
```
通常,析构函数是空的,但是在这里,它有日志来显示行为。
6. 从该接口类中派生`巫师`、`治疗者`和`战士`类,并在每个类的`speak()`和`act()`函数的声明末尾添加`override`关键字:
```cpp
class Wizard : public Icharacter { ...
```
7. Click the **Run** button to rebuild and run the exercise. We will now see that the base class destructor is also called after the destructor of the derived class:

###### 图 2B.7:修改后程序的输出
8. 创建角色并在一个容器中管理它们,例如`向量`。在文件中创建以下两种方法,在`主()`功能之前:
```cpp
void createCharacters(std::vector& cast)
{
cast.push_back(new Wizard("Gandalf"));
cast.push_back(new Healer("Glenda"));
cast.push_back(new Warrior("Ben Grimm"));
}
void freeCharacters(std::vector& cast)
{
for(auto* character : cast)
{
delete character;
}
cast.clear();
}
```
9. 将`main()`的内容替换为如下代码:
```cpp
int main(int argc, char**argv)
{
std::cout << "\n------ Exercise 1 ------\n";
std::vector cast;
createCharacters(cast);
for(auto* character : cast)
{
character->speak();
}
for(auto* character : cast)
{
character->act();
}
freeCharacters(cast);
std::cout << "Complete.\n";
return 0;
}
```
10. Click the **Run** button to rebuild and run the exercise. Here is the output that is generated:

###### 图 2B.8:多态版本的输出
从前面的截图可以看到,**摧毁精灵**等的日志已经消失。问题是容器保存指向基类的指针,并且它不知道如何在每种情况下调用完整的析构函数。
11. 要解决这个问题,只需将`的析构函数`声明为虚拟的:
```cpp
virtual ~ICharacter() {
```
12. 点击**运行**按钮重建并运行练习。输出内容如下:

###### 图 2B.9:完整多态版本的输出
我们现在已经实现了一个到我们的`ICharacter`字符的接口,并通过存储在容器中的基类指针简单地调用`speak()`和`act()`方法来多形态地使用它们。
### 重新审视类、结构和联合
之前,我们讨论过类和结构之间的区别是默认的访问修饰符——类是私有的,结构是公共的。这种差异更进一步——如果基类没有指定任何内容,它也适用于基类:
```cpp
class DerivedC : Base // inherits as if "class DerivedC : private Base" was used
{
};
struct DerivedS : Base // inherits as if "struct DerivedS : public Base" was used
{
};
```
应该注意的是,联合既不能是基类,也不能从基类派生。如果结构和类本质上没有区别,那么我们应该使用哪种类型呢?本质上,这是一个惯例。一个**结构**用来捆绑几个相关的元素,而一个**类**可以做事,有责任。结构的示例如下:
```cpp
struct Point // A point in 3D space
{
double m_x;
double m_y;
double m_z;
};
```
在前面的代码中,我们可以看到它将三个坐标组合在一起,这样我们就可以对三维空间中的一个点进行推理。这个结构可以作为一个连贯的数据集传递给需要点的方法,而不是每个点三个单独的参数。另一方面,类对可以执行动作的对象进行建模。看看下面的例子:
```cpp
class Matrix
{
public:
Matrix& operator*(const Matrix& rhs)
{
// nitty gritty of the multiplication
}
private:
// Declaration of the 2D array to store matrix.
};
```
经验法则是,如果至少有一个私有成员,就使用一个类,因为这意味着实现的细节将在公共成员函数的后面。
## 可见性、寿命和访问
我们已经讨论了创建自己的类型和声明变量和函数,同时主要关注简单函数和单个文件。我们现在将看看当有多个包含类和函数定义的源文件(翻译单元)时会发生什么。此外,我们将检查哪些变量和函数可以从源文件的其他部分看到,这些变量存在多长时间,并查看内部和外部链接之间的区别。在*第 1 章*、*剖析可移植 C++ 软件*中,我们看到了工具链是如何编译源文件并生成目标文件的,以及链接器是如何将它们组合在一起形成可执行程序的。
当编译器处理源文件时,它会生成一个目标文件,该文件包含已翻译的 C++ 代码和足够的信息,以便链接器解析从已编译的源文件到另一个源文件的任何引用。在*第 1 章*、*解析可移植 C++ 软件*、 **CxxTemplate.cpp** 中称为`sum()`,在 **SumFunc.cpp** 文件中定义。当编译器构造一个目标文件时,它会创建以下段:
* **代码段**(也称文本):这是将 C++ 函数翻译成目标机器指令。
* **数据段**:包含程序中声明的所有变量和数据结构,不是本地的,也不是从堆或栈中分配的,并且是初始化的。
* **BSS 段**:包含程序中声明的所有变量和数据结构,不是本地的,也不是从堆或栈中分配的,并且没有初始化(但是将被初始化为零)。
* **导出符号数据库**:该对象文件中变量和函数的列表及其位置。
* **Database of referenced symbols**: A list of variables and functions this object file needs from outside itself and where they are used.
#### 注意
BSS 用于命名未初始化的数据段,其名称历史上来源于以符号开始的块。
然后,链接器将所有代码段、数据段和 **BSS** 段收集在一起,形成程序。它使用两个数据库(DB)中的信息将所有引用的符号解析到导出的符号列表中,并用该信息修补代码段,以便它们可以正确运行。从图形上看,这描述如下:

###### 图 2B.10:部分目标文件和可执行文件
出于以下讨论的目的,基站和数据段将简称为数据段(唯一的区别是基站未初始化)。当一个程序被执行时,它被加载到内存中,并且它的内存看起来有点像可执行文件的布局——它包含文本段、数据段、BSS 段和主机系统分配的空闲内存,后者包含所谓的**栈**和**堆**。堆栈通常从内存顶部开始向下增长,而堆从 BSS 结束的地方开始向上增长,朝向堆栈:

###### 图 2B.11: CxxTemplate 运行时内存映射
程序中可访问变量或标识符的部分称为**范围**。有两大类范围:
* **本地范围**(也称为**块范围**):这适用于用花括号(`{}`)括起来的块内声明的任何内容。变量可以在大括号内访问。就像块可以嵌套一样,变量的范围也可以嵌套。这通常包括局部变量和函数参数,它们通常存储在堆栈中。
* **全局/文件范围**:这适用于在正常函数或类之外声明的变量,也适用于正常函数。如果链接正确,变量可以在文件的任何地方访问,也可以从其他文件(全局)访问。这些变量由数据段中的链接器分配内存。标识符被放入全局命名空间,这是默认命名空间。
### 命名空间
我们可以把命名空间看作是变量、函数和用户定义类型的字典。对于小程序,使用全局命名空间是可以的,因为创建多个同名变量并产生名称冲突的可能性很小。随着程序变得越来越大,包括了更多的第三方库,名字冲突的机会增加了。因此,库作者将把他们的代码放入一个命名空间(希望是唯一的)。这允许程序员控制对命名空间中标识符的访问。通过使用标准库,我们已经使用了 std 命名空间。命名空间是这样声明的:
```cpp
namespace name_of_namespace { // put declarations in here }
```
命名空间的名称通常很短,命名空间可以嵌套。
#### 注意
在这里的 boost 库中可以看到名称空间的良好使用:[https://www.boost.org/](https://www.boost.org/)。
变量还有另一个属性,即**寿命**。有三种基本寿命;两个由编译器管理,一个由程序员选择:
* **自动生存期**:局部变量在声明时创建,并在退出其所在的范围时销毁。这些由堆栈管理。
* **永久寿命**:全局变量和静态局部变量。编译器在程序开始时(进入 main()函数之前)创建全局变量,并在首次访问静态局部变量时创建静态局部变量。在这两种情况下,当程序退出时,变量都会被销毁。这些变量由链接器放在数据段中。
* **动态寿命**:变量是根据程序员的请求创建和销毁的(通过使用`新增`和`删除`)。这些变量从堆中分配内存。
我们将考虑的变量的最后一个属性是**联动**。链接表示如果编译器和链接器遇到具有相同名称(或标识符)的变量和函数,它们会做什么。对于一个函数来说,它实际上就是所谓的变形名——编译器使用函数的名称、返回类型和参数类型来产生一个变形名。有三种类型的链接:
* **无链接**:这意味着标识符只引用自身,适用于局部变量和局部定义的用户类型(即块内部)。
* **内部链接**:这意味着标识符可以在声明它的文件中的任何地方被访问。这适用于静态全局变量、常量全局变量、静态函数以及文件中匿名命名空间中声明的任何变量或函数。匿名命名空间是没有指定名称的命名空间。
* **外部链接**:这意味着通过右向声明,可以从所有文件内部访问。这包括普通函数、非静态全局变量、外部常量全局变量和用户定义的类型。
虽然这些被称为连接,只有最后一个实际上涉及连接。另外两个是通过编译器从导出标识符的数据库中排除信息来实现的。
## 模板–通用编程
作为一名计算机科学家,或者作为一名编程爱好者,在某个时间点,你可能不得不编写一个(或多个)排序算法。在讨论算法时,您并不特别关心正在排序的数据类型,只是该类型的两个对象可以进行比较,并且该域是一个完全有序的集合(也就是说,如果一个对象与任何其他对象进行比较,您可以确定哪个先出现)。不同的编程语言对此问题提供了不同的解决方案:
* **Python** :内置函数排序、列表上成员函数的动态语言。作为一种动态语言,如果能够调用比较运算符和`交换`函数,就不需要关心类型。
* **C**: This has a function in its standard library called qsort that has the following signature:
```cpp
void qsort (void* base, size_t num, size_t size, int (*compare)(const void*,const void*));
```
这处理不同的类型,因为基础是一个`空指针`。`size_t` size 定义每个对象的大小,而`compare()`函数定义如何比较两个对象。
* **C++**: `std::sort()` is a function provided in its standard library, where one of its signatures is as follows:
```cpp
template< class RandomIt > void sort( RandomIt first, RandomIt last );
```
在这种情况下,类型的细节在称为`随机化`的迭代器类型中捕获,并在编译时传递给方法。
在下一节中,我们将简要定义泛型编程,展示 C++ 如何通过模板实现它们,突出显示该语言已经提供了什么,并讨论编译器如何推导类型,以便它们可以用于模板。
### 什么是泛型编程?
当您开发排序算法时,您可能最初只关注对普通数字进行排序。但是一旦建立了这种关系,您就可以将它抽象为任何类型,只要该类型表现出某些属性,例如总有序集(也就是说,比较运算符
**泛型编程**是一种类型不可知的通用算法的开发。通过将类型作为参数传递,可以重用该算法。这样,算法被抽象,并允许编译器基于类型进行优化。
换句话说,泛型编程是一种编程方法,在这种方法中,算法是用类型定义的,而类型是在算法实例化时指定的参数。许多语言支持不同名称的泛型编程。在 C++ 中,泛型编程通过称为模板的语言特性得到支持。
### 介绍 C++ 模板
模板是 C++ 对泛型编程的支持。把一个模板想象成一个饼干切割器,我们给它的类型作为一个参数,比如饼干面团(可以是巧克力布朗尼,姜片,或者其他美味的味道)。当我们应用 cookie cutter 时,我们最终会得到形式相同但口味不同的 cookie 实例。因此,模板捕获泛型函数或类的定义,当用类型作为参数指定时,编译器开始为我们编写类或函数,就好像类型是由我们手工编码的一样。它有几个优点,例如:
* 您只需要开发一次类或算法并对其进行进化。
* 您可以将其应用于许多类型。
* 您可以将复杂的细节隐藏在简单的接口后面,编译器可以根据类型对生成的代码进行优化。
那么,我们如何编写模板呢?让我们从一个模板开始,该模板允许我们在从`lo`到`hi`的范围内夹紧一个值,并且能够在`int`、`float`、`double`或任何其他内置类型上使用该值:
```cpp
template
T clamp(T val, T lo, T hi)
{
return (val < lo) ? lo : (hi < val) ? hi : val;
}
```
让我们把它分解一下:
* **第 1 行** : `模板<类 T >`声明后面的内容为模板,使用一种类型,模板中有一个`T`的占位符。
* **第 2 行**:当`T`被替换时,声明该功能的原型。它声明函数 clamp 接受三个类型为`T`的参数,并返回一个类型为`T`的值。
* **第 4 行**:这就是模板的妙处——假设传入的类型有一个`<`操作符,那么我们就可以对这三个值进行钳制,这样`lo < = val < = hi`。该算法对所有可排序的类型都有效。
假设我们在下面的程序中使用它:
```cpp
#include
int main()
{
std::cout << clamp(5, 3, 10) << "\n";
std::cout << clamp(3, 5, 10) << "\n";
std::cout << clamp(13, 3, 10) << "\n";
std::cout << clamp(13.0, 3.0, 10.1) << "\n";
std::cout << clamp(13.0, 3, 10.2) << "\n";
return 0;
}
```
我们将获得以下预期输出:

###### 图 2B.12:箝位程序输出
在最后一次调用夹钳时,我们已经在`<`和`>`之间传递了模板的双重类型。但是其他四个电话我们没有遵循同样的规则。为什么呢?事实证明,随着年龄的增长,编译器变得越来越聪明。随着标准的每一次发布,他们改进了所谓的**式演绎**。因为编译器能够推导出类型,所以我们不需要告诉它使用什么类型。这样做的原因是,没有模板参数的类的三个参数具有相同的类型——前三个都是 int,而第四个是 double。但是我们必须告诉编译器最后一个使用哪种类型,因为它有两个 doubles 和一个 int 作为参数,这导致了一个编译错误,说没有找到函数。但是后来,它给了我们为什么模板不能被使用的信息。这种强制类型的形式被称为**显式模板参数规范**。
### C++ 预打包模板
C++ 标准由两个主要部分组成:
* 语言定义,即关键词、句法、词汇定义、结构等。
* 标准库,即编译器供应商提供的所有预写的通用函数和类。这个库的一个子集是使用模板实现的,被称为**标准模板库** ( **STL** )。
STL 起源于大卫·穆塞和亚历山大·斯捷潘诺夫开发的 Ada 语言中提供的泛型。斯捷潘诺夫大力提倡使用通用编程作为软件开发的基础。在 90 年代,他看到了用新语言 C++ 来影响主流开发的机会,并向 ISO C++ 委员会提议将 STL 作为语言的一部分。剩下的就是历史。
STL 由四类预定义的通用算法和类组成:
* **容器**:一般序列(向量、列表、德格)和关联容器(集合、多集合、映射)
* **迭代器**:一组遍历容器并定义容器范围的类(范围表示为`begin()`和`end()`)。请注意,STL 中的一个基本设计选择是`end()`指向最后一项之后的一个位置–数学上,即[ `begin()`,`end()`)。
* **算法**:超过 100 种不同的算法,涵盖排序、搜索、集合运算等。
* **函数**:支持函子(可以像函数一样调用对象的函数对象)。一个用途是模板算法中的谓词,如`find_if()`。
我们之前实现的箝位函数模板过于简单,虽然它适用于任何支持小于运算符的类型,但效率不是很高——如果类型很大,可能会产生非常大的副本。从 C++ 17 开始,STL 包含了一个`std::clamp()`函数,声明如下:
```cpp
#include
template
const T& clamp( const T& v, const T& lo, const T& hi, Compare comp )
{
return assert( !comp(hi, lo) ),
comp(v, lo) ? lo : comp(hi, v) ? hi : v;
}
template
const T& clamp( const T& v, const T& lo, const T& hi )
{
return clamp( v, lo, hi, std::less<>() );
}
```
正如我们所看到的,它使用参数和返回值的引用。将参数更改为使用引用减少了堆栈上必须传递和返回的内容。此外,请注意,设计人员已经制作了一个更通用的模板版本,这样我们就不会依赖于该类型存在的
从前面的例子中,我们已经看到,像函数一样,模板可以采用多个逗号分隔的参数。
## 类型别名–类型定义和使用
如果你使用了`std::string`类,那么你就使用了一个别名。有几个与字符串相关的模板类需要实现相同的功能。但是代表一个角色的类型是不同的。例如对于`std::string`,表示为`char`,而`std::wstring`则使用`wchar_t`。`char16_t`和`char32_t`还有其他几个。功能的任何变化都将通过特性或模板专门化来管理。
在 C++ 11 之前,这可能是从`std::basic_string`基类别名而来的,如下所示:
```cpp
namespace std {
typedef basic_string string;
}
```
这有两个主要作用:
* 减少声明变量所需的键入量。这是一个简单的情况,但是当您声明一个指向字符串到对象的映射的唯一指针时,它会变得很长,并且您会出错:
```cpp
typedef std::unique_ptr> UptrMapStrToClass;
```
* 提高可读性,因为您现在在概念上将它视为一个字符串,不需要担心细节。
但是 C++ 11 引入了一个更好的方法——别名声明,它使用关键字来使用**。前面的代码可以这样实现:**
```cpp
namespace std {
using string = basic_string;
}
```
前面的例子很简单,别名,无论是 typedef 还是 using,都不难理解。但是当别名涉及更复杂的表达式时,它们也可能有点不可读——尤其是函数指针。考虑以下代码:
```cpp
typedef int (*FunctionPointer)(const std::string&, const Point&);
```
现在,考虑以下代码:
```cpp
using FunctionPointer = int (*)(const std::string&, const Point&);
```
C++ 11 中的新特性是有原因的,其中别名声明可以很容易地合并到模板中——它们可以被模板化。一个`typedef`不能被模板化,虽然可以用`typedef`获得相同的结果,但是别名声明(`使用`)是首选方法,因为它导致模板代码更简单和更容易理解。
### 练习 2:实现别名
在本练习中,我们将使用 typedef 实现别名,并了解代码如何通过使用引用变得更容易阅读和更高效。按照以下步骤实施本练习:
1. 在 Eclipse 中打开**第 2B 课**项目,然后在项目浏览器中展开**第 2B 课**,然后展开**练习 02** ,双击**练习 2.cpp** 在编辑器中打开本练习的文件。
2. 点击**启动配置**下拉菜单,选择**新启动配置……**。将 **L2BExercise2** 配置为以**练习 2** 的名称运行。完成后,它将是当前选定的启动配置。
3. Click on the **Run** button. **Exercise 2** will run and produce something similar to the following output:

###### 图 2B.13:练习 2 输出
4. 在编辑器中,在声明`打印矢量()`函数之前,添加以下行:
```cpp
typedef std::vector IntVector;
```
5. 现在,用`IntVector`更改文件中所有出现的`std::vector < int >`。
6. 点击**运行**按钮。输出应该和以前一样。
7. 在编辑器中,将之前添加的行更改为:
```cpp
using IntVector = std::vector;
```
8. 点击**运行**按钮。输出应该和以前一样。
9. 在编辑器中,添加以下行:
```cpp
using IntVectorIter = std::vector::iterator;
```
10. 现在,将`IntVector::iterator`的一次出现更改为`int vector。`
11. 点击**运行**按钮。输出应该和以前一样。
在本练习中,typedef 和使用 alias 之间似乎没有什么区别。在这两种情况下,使用一个好名字的别名使代码更容易阅读和理解。当涉及到更复杂的别名时,`使用`会产生一种更简单的别名书写方式。在 C++ 11 中引入,`使用`现在是定义别名的首选方法。它比`typedef`还有其他优势,比如可以在模板里面使用。
## 模板–不仅仅是通用编程
模板也可以提供比一般编程更多的东西。在泛型编程的情况下,模板作为不能更改的蓝图运行,并为指定的一个或多个类型提供模板的编译版本。
可以根据所涉及的类型编写模板来提供函数或算法的专门化。这被称为**模板专门化**,从我们之前使用的意义上来说,它不是泛型编程。只有当它使某些类型在给定的上下文中按照我们期望的那样运行时,它才能被称为泛型编程。当用于所有类型的算法被修改时,它不能被称为泛型编程。
检查以下专门化代码示例:
```cpp
#include
#include
template = 0>
void print(T val)
{
printf("%c\n", val);
}
template = 0>
void print(T val)
{
printf("%d\n", val);
}
template = 0>
void print(T val)
{
printf("%f\n", val);
}
int main(int argc, char** argv)
{
print('c');
print(55);
print(32.1F);
print(77.3);
}
```
它定义了一个使用不同格式字符串调用`printf()`的模板,基于使用`std::enable_if_t < >`和`sizeof()`的模板的专门化。当我们运行它时,会生成以下输出:

###### 图 2B.14:错误的打印模板程序输出
### 替代失败不是错误–SFINAE
为`32.1F`(`-1073741824`)打印的数值与该数字没有任何相似之处。如果我们检查编译器为以下程序生成的代码,我们会发现它已经生成了代码,就像我们编写了以下内容(以及更多内容)一样:
```cpp
template
void print(int val)
{
printf("%d\n",val);
}
template
void print(float val)
{
printf("%d\n", val);
}
```
它为什么会生成这个代码?前面的模板使用了 C++ 编译器的一个名为 **S** 的特性来代替 **F** 故障 **I** s **N** ot **A** n 错误,或 **SFINAE** 。基本上,在模板的替换阶段,基于类型,如果编译器不能形成有效的代码,那么它只是丢弃定义并继续,而不是产生错误。让我们尝试修复前面的代码,并获得正确的打印结果。为此,我们将介绍`STD::enable _ if _ t<>`的用法,并访问所谓的**类型特征**来帮助我们。首先,我们将使用以下代码替换最后一个模板:
```cpp
#include
template , int> = 0>
void print(T val)
{
printf("%f\n", val);
}
```
这需要一些解释。首先我们考虑`std::enable_if_t`的定义,其实是一个类型别名:
```cpp
template
struct enable_if {};
template
struct enable_if { typedef T type; };
template< bool B, class T = void >
using enable_if_t = typename enable_if::type;
```
`enable_if`的第一个模板将导致空结构(或类)的定义。`enable_if`的第二个模板是 true 的特化,作为第一个模板参数,它将产生一个具有 typedef 定义的类。`enable_if_t`的定义是一个助手模板,它免去了我们在使用时输入`:在模板末尾键入`的需要。那么,这是如何工作的呢?考虑以下代码:
```cpp
template = 0>
void print(T val) { … }
```
如果在编译时评估的条件导致**为真**,则`enable_if_t`模板将导致如下模板:
```cpp
template
void print(T val) { … }
```
这是有效的语法,该函数作为候选函数添加到符号表中。如果在编译时计算的条件导致**为假**,那么`enable_if_t`模板将生成如下所示的模板:
```cpp
template
void print(T val) { … }
```
这是**格式错误的代码**,现在被丢弃了——SFINAE 在工作。
`STD::is _ floating _ point _ v`是另一个访问`STD::is _ floating _ point`模板的`:值`成员的辅助类。它的名字说明了一切——如果 T 是浮点类型(float、double、long double),那就是真的;否则,它将是假的。如果我们进行此更改,编译器(GCC)将生成以下错误:

###### 图 2B.15:修改后的打印模板程序的编译器错误
现在的问题是,当类型是 float 时,我们有两个模板可以满足:
```cpp
template = 0>
void print(T val)
{
printf("%d\n", val);
}
template , int> = 0>
void print(T val)
{
printf("%f\n", val);
}
```
原来(通常)`sizeof(float)= = sizeof(int)`,所以我们需要再做一个改动。我们将第一个条件替换为另一个类型特征–`STD::is _ integral _ v<>:`
```cpp
template , int> = 0>
void print(T val)
{
printf("%d\n", val);
}
```
如果我们进行此更改,编译器(GCC)将生成以下错误:

###### 图 2B.16:修改后的打印模板程序的第二个编译器错误
我们修复了浮点模糊性,但是这里的问题是 **std::is_integral_v(char)** 返回 true,同样有两个函数是由具有相同原型的 char 类型的模板生成的。事实证明,传递给**的条件遵循标准的 C++ 逻辑表达式。因此,为了解决这个问题,我们将添加一个排除字符的额外条件:**
```cpp
template && sizeof(T) != 1, int> = 0>
void print(T val)
{
printf("%d\n", val);
}
```
如果我们现在编译程序,它会完成编译并链接程序。如果我们运行它,它现在会产生以下(预期的)输出:

###### 图 2B.17:修正的打印模板程序输出
### 浮点表示
那`32.099998`不应该是`32.1`吗?这就是传递给函数的内容。在计算机上执行浮点运算的问题是表示会自动引入错误。实数形成一个连续(无限)的域。如果你考虑实数域中的数字 1 和 2,那么它们之间有无限多的实数。不幸的是,计算机对浮点数的表示量化了这些值,并且不能表示所有的无限数量的数字。用于存储数字的位数越大,该值在实数域上的表示就越好。所以,长双优于双优于浮。关于什么适合存储数据,这实际上取决于您的问题领域。回到`32.099998`。计算机将单精度数字存储为 2 的幂之和,然后将它们移动一个幂因子。整数通常很容易,因为它们很容易用`2^n`次幂之和(n > =0)来表示。分数部分,在这种情况下是 0.1,必须表示为`2^(-n(n>0)`的和。我们增加更多的 2 次方分数,试图使数字更接近目标值,直到我们用完单个精确浮点数的 24 位精度。
#### 注意
如果你想知道更多关于计算机如何存储浮点数的知识,研究一下定义浮点数的 IEEE 754 标准。
### 常量表达式 if 表达式
C++ 17 在语言中引入了`constexpr if`表达式,大大简化了模板编写。我们可以重写前面三个使用 SFINAE 作为一个更简单模板的模板:
```cpp
#include
#include
template
void print(T val)
{
if constexpr(sizeof(T)==1) {
printf("%c",val);
}
else if constexpr(std::is_integral_v) {
printf("%d",val);
}
else if constexpr(std::is_floating_point_v) {
printf("%f",val);
}
printf("\n");
}
int main(int argc, char** argv)
{
print('c');
print(55);
print(32.1F);
print(77.3);
}
```
对于`打印(55)`的调用,编译器生成如下函数进行调用:
```cpp
template<>
void print(int val)
{
printf("%d",val);
printf("\n");
}
```
if/else if 语句怎么了?如果表达式为**常量表达式,编译器会根据上下文确定条件的值,并将其转换为布尔值(真/假)。如果计算值为真,则 If 条件和 else 子句被丢弃,只留下 true 子句来生成代码。同样,如果它是 false,那么 false 子句将被留下来生成代码。换句话说,只有计算结果为 true 的第一个 constexpr if 条件将生成其子句的代码,而其余的将被丢弃。**
### 非类型模板参数
到目前为止,我们只看到了属于类型的模板参数。也可以传递整数值作为模板参数。这允许我们防止函数的数组衰减。例如,考虑一个计算`和`的模板函数:
```cpp
template
T sum(T data[], int number)
{
T total = 0;
for(auto i=0U ; i
T sum(T (&data)[size])
{
T total = 0;
for(auto i=0U ; i< size; i++)
{
total += data[i];
}
return total;
}
```
在这里,我们将数据更改为对某个特定大小的数组的引用,该大小被传递给模板,因此编译器会计算出来。我们不再需要函数调用的第二个参数。这个简单的例子展示了如何直接传递和使用非类型参数。我们将在*模板类型演绎*部分对此进行更多探讨。
### 练习 3:实现 Stringify–专门化与常量表达式
在本练习中,我们将通过使用 constexpr 来实现一个字符串模板,以生成一个更容易阅读和更简单的代码版本。按照以下步骤实施本练习:
#### 注意
字符串化的专门化模板可以在[https://isocpp . org/wiki/FAQ/templates # templates-专门化-示例](https://isocpp.org/wiki/faq/templates#template-specialization-example)中找到。
1. 在 Eclipse 中打开**第 2B 课**项目,然后在**项目浏览器**中,展开**第 2B 课**,然后展开**练习 03** ,双击**练习 3.cpp** 在编辑器中打开本练习的文件。
2. 点击**启动配置**下拉菜单,选择**新启动配置……**。将**2 练习 3** 配置为使用名称**练习 3** 运行。
3. Click on the **Run** button. **Exercise 3** will run and produce the following output:

###### 图 2B.18:练习 3 专用模板输出
4. 在**练习 3.cpp** 中,注释掉字符串模板的所有模板专门化,同时保留原始的通用模板。
5. Click on the **Run** button. The output will change to have the boolean printed as a number and the double printed to only two decimal places:

###### 图 2B.19:练习 3 仅输出通用模板
6. 我们现在将再次“专门化”布尔类型的模板。将`#包括<类型 _ 特征>`指令与其他`#包括`指令相加,并修改模板,使其内容如下:
```cpp
template std::string stringify(const T& x)
{
std::ostringstream out;
if constexpr (std::is_same_v)
{
out << std::boolalpha;
}
out << x;
return out.str();
}
```
7. Click on the **Run** button. The output boolean stringify works as before:

###### 图 2B.20:为布尔函数定制的字符串
8. 我们现在将再次“专门化”浮点类型的模板(`float`、`double`、`long double`)。修改模板,使其如下所示:
```cpp
template std::string stringify(const T& x)
{
std::ostringstream out;
if constexpr (std::is_same_v)
{
out << std::boolalpha;
}
else if constexpr (std::is_floating_point_v)
{
const int sigdigits = std::numeric_limits::digits10;
out << std::setprecision(sigdigits);
}
out << x;
return out.str();
}
```
9. Click on the **Run** button. The output is restored to the original:

###### 图 2B.21: constexpr if 版本模板输出
10. 如果将有多个模板的原始版本与最终版本进行比较,你会发现最终版本更像是一个正常的功能,更容易阅读和维护。
在练习中,我们了解了在 C++ 17 中使用新的 constexpr if 构造时,我们的模板可以变得多么简单和紧凑。
### 函数重载再探
当我们第一次讨论函数重载时,我们只考虑了函数名来自手工编写的函数列表的场景。现在,我们需要更新这个。我们还可以编写具有相同名称的模板化函数。就像我们之前做的那样,当编译器遇到行`print(55)`时,它需要计算出要调用哪个先前定义的函数。因此,它执行以下过程(非常简单):

###### 图 2B.22:使用模板的函数重载解析(简化)
### 模板类型演绎
当我们第一次引入模板时,我们触及了模板类型演绎。现在,我们将进一步探讨这个问题。我们将首先考虑函数模板的一般声明:
```cpp
template
void function(ParamType parameter);
```
对此的呼吁可能是这样的:
```cpp
function(expression); // deduce T and ParamType from expression
```
当编译器到达这一行时,它现在必须推导出与模板相关的两个类型–`T`和`ParamType`。由于参数类型中附加到 T 的限定符和其他属性(例如指针、引用、常量等),它们通常是不同的。类型是相关的,但是演绎的进程是不同的,这取决于所使用的`表达的形式`。
### 显示推导出的类型
在我们研究不同的形式之前,如果我们能让编译器告诉我们它已经推导出的类型,这可能是有用的。这里我们有几个选项,包括显示类型的 IDE 编辑器、生成错误的编译器和运行时支持(由于 C++ 标准,这不一定有效)。我们将使用编译器错误来帮助我们探索一些类型推断。
我们可以通过声明一个没有定义的模板来实现一个类型显示器。任何实例化模板的尝试都会导致编译器生成一条错误消息,因为没有定义以及它试图实例化的类型信息:
```cpp
template
struct TypeDisplay;
```
让我们尝试编译以下程序:
```cpp
template
class TypeDisplay;
int main()
{
signed int x = 1;
unsigned int y = 2;
TypeDisplay x_type;
TypeDisplay y_type;
TypeDisplay x_y_type;
return 0;
}
```
编译器会抛出以下错误:

###### 图 2B.23:显示推断类型的编译器错误
请注意,在每种情况下,被命名的聚合都包括被推导的类型——对于 x,它是一个 int,对于 y,它是一个无符号 int,对于 x+y,它是一个无符号 int。另外,请注意,TypeDisplay 模板需要一个类型作为其参数,因此使用`decltype()`函数让编译器为括号中的表达式提供类型。
也可以使用内置的`类型标识(T)在运行时显示推导出的类型。name()`运算符,该运算符返回一个 std::string,或者使用名为 type_index 的 boost 库。
#### 注意
更多信息,请访问以下链接:[https://www . boost . org/doc/libs/1 _ 70 _ 0/doc/html/boost _ type index . html](https://www.boost.org/doc/libs/1_70_0/doc/html/boost_typeindex.html)。
因为类型推演规则,内置运算符会给你一个类型的指示,但是会丢失引用(`&`、`& &`)和任何常量信息(常量或挥发)。如果在运行时需要,那么考虑`boost::type_index`,这将为所有编译器产生相同的输出。
### 模板类型演绎-细节
让我们回到通用模板:
```cpp
template
void function(ParamType parameter);
```
假设电话是这样的:
```cpp
function(expression); // deduce T and ParamType from expression
```
根据所用参数类型的形式,类型推导的进行方式不同:
* **ParamType 是一个值(T)** :按值传递函数调用
* **ParamType 是引用或指针(T &或 T*)** :通过引用传递函数调用
* **ParamType 是一个右值引用(T & & )** :通过引用传递函数调用或者别的什么
**情况 1: ParamType 是传递值(T)**
```cpp
template
void function(T parameter);
```
作为一个按值传递的调用,这意味着参数将是传入的任何内容的副本。因为这是对象的新实例,所以以下规则应用于表达式:
* 如果表达式的类型是引用,则忽略引用部分。
* 如果在步骤 1 之后,剩余的类型是常量和/或易失性的,那么也忽略它们。
剩下的就是 t .让我们尝试编译以下文件代码:
```cpp
template
class TypeDisplay;
template
void function(T parameter)
{
TypeDisplay type;
}
void types()
{
int x = 42;
function(x);
}
```
编译器会产生以下错误:

###### 图 2B.24:编译器错误显示了按类型传递的推断类型
所以,类型推导为`int`。同样,如果我们声明以下内容,我们会得到完全相同的错误:
```cpp
const int x = 42;
function(x);
```
如果我们声明这个版本,也会发生同样的情况:
```cpp
int x = 42;
const int& rx = x;
function(rx);
```
根据前面所述的规则,在所有三种情况下,推导出的类型都是`int`。
**情况 2: ParamType 是通过引用传递的(T & )**
作为一个按引用传递的调用,这意味着参数将能够访问对象的原始存储位置。正因为如此,生成的函数必须尊重我们之前忽略的常量和可变性。以下规则适用于类型扣减:
* 如果表达式的类型是引用,则忽略引用部分。
* 模式将表达式类型的剩余部分与参数类型进行匹配,以确定 t
让我们尝试编译以下文件:
```cpp
template
class TypeDisplay;
template
void function(T& parameter)
{
TypeDisplay type;
}
void types()
{
int x = 42;
function(x);
}
```
编译器将生成以下错误:

###### 图 2B.25:显示通过引用传递的推导类型的编译器错误
由此我们可以看出,编译器把 T 作为 **int** 从 ParamType 作为**int&T3。将 x 更改为常量 int 并不意外,因为从 ParamType 推导出 T 是**常量 int** 为**常量 int &** :**

###### 图 2B.26:编译器错误显示了常量引用传递的推导类型
同样,像以前一样,引入 rx 作为常量 int 的引用,也不会让人感到意外,因为从 ParamType 推导出 T 是`常量 int`作为`常量 int &`:
```cpp
void types()
{
const int x = 42;
const int& rx = x;
function(rx);
}
```

###### 图 2B.27:在传递常量引用时显示推导类型的编译器错误
如果我们将声明更改为包含一个常量,那么编译器将在从模板生成函数时遵守该常量:
```cpp
template
void function(const T& parameter)
{
TypeDisplay type;
}
```
这一次,编译器会报告以下内容
* `int x` : T 是 int(因为常量会被尊重),而参数的类型是`const int &`。
* `const int x` : T 是 int (const 在模式中,保留 int),而参数的类型是`const int &`。
* `const int & rx` : T 是 int(引用被忽略,const 在模式中,留下 int),而参数的类型是`const int &`。
如果我们试图编译以下内容,我们期望什么?通常,数组衰减为指针:
```cpp
int ary[15];
function(ary);
```
编译器错误如下:

###### 图 2B.28:编译器错误,显示了通过引用传递数组参数时推导出的类型
这一次,数组被捕获作为参考,大小也被包括在内。所以,如果 ary 被声明为`ary【10】`,那么将会产生一个完全不同的函数。让我们将模板还原为以下内容:
```cpp
template
void function(T parameter)
{
TypeDisplay type;
}
```
如果我们试图编译数组调用,那么错误报告如下:

###### 图 2B.29:通过值传递数组参数时显示推导出的类型的编译器错误
我们可以看到,在这种情况下,当将数组传递给函数时,数组已经像通常的行为一样衰减了。我们在谈论*非类型模板参数*时看到了这种行为。
**情况 3: ParamType 是右值引用(T & & )**
& T 被称为右值引用,而& T 被称为左值引用。C++ 不仅通过类型来表征表达式,还通过名为**值类别**的属性来表征表达式。这些类别控制编译器中的表达式计算,包括创建、复制和移动临时对象的规则。C++ 17 标准中定义了五个表达式值类别,它们具有以下关系:

###### 图 2B.30: C++ 值类别
每个的定义如下:
* 确定对象身份的表达式是`glvalue`。
* 一个表达式,其求值初始化一个对象或一个运算符的操作数是一个`prvalue`。示例包括文字(字符串文字除外),如 3.1415、true 或 nullptr、this 指针、后置递增和后置递减表达式。
* 有资源并且可以重用(因为它的生命即将结束)的 glvalue 对象是`xvalue`。例如,函数调用的返回类型是对对象的右值引用,如`标准::移动()`。
* 不是 x 值的 GL 值是`左值`。示例包括变量名、函数名、数据成员名或字符串。
* prvalue 或 xvalue 是一个`值`。
如果您对下面的解释不完全理解,也没关系——只要知道被认为是左值的表达式可以使用它的地址(使用运算符的地址,即“&”)。以下内容的类型推导规则要求您知道左值是什么,以及它不是什么:
```cpp
template
void function(T&& parameter)
{
TypeDisplay type;
}
```
此参数类型表单的类型推导规则如下:
* 如果表达式是左值引用,那么 T 和 ParamType 都被推导为左值引用。这是类型被推断为引用的唯一场景。
* 如果表达式是右值引用,则情况 2 的规则适用。
### SFINAE 表达式和尾随返回类型
C++ 11 引入了一个名为`尾随返回类型`的特性,为模板提供了一种机制,这样它们就可以概括返回类型。一个简单的例子如下:
```cpp
template
auto mul(T a, T b) -> decltype(a * b)
{
return a * b;
}
```
这里,`auto`用来表示定义了一个尾随返回类型。尾部返回类型以`- >`指针开始,在这种情况下,返回类型是通过将`a`和`b`相乘而返回的类型。编译器将处理 decltype 的内容,如果它的格式不正确,它将像往常一样从函数名的查找中删除定义。该功能提供了许多可能性,因为逗号运算符“`、`”可以在`decltype`中使用,以检查某些属性。
如果我们想测试一个类实现了一个方法或者包含了一个类型,那么我们可以把它放在 decltype 里面,方法是把它转换成一个 void(以防逗号操作符被重载),然后在逗号操作符的末尾定义一个真正返回类型的对象。下面的程序显示了一个这样的例子:
```cpp
#include
#include
#include
#include
#include
template
auto contains(const C& c, const T& x)
-> decltype((void)(std::declval().find(std::declval())), true)
{
return end(c) != c.find(x);
}
int main(int argc, char**argv)
{
std::cout << "\n\n------ SFINAE Exercise ------\n";
std::set mySet {1,2,3,4,5};
std::cout << std::boolalpha;
std::cout << "Set contains 5: " << contains(mySet,5) << "\n";
std::cout << "Set contains 15: " << contains(mySet,15) << "\n";
std::cout << "Complete.\n";
return 0;
}
```
当这个程序被编译和执行时,我们获得以下输出:

###### 图 2 b . 31:SFINAE 表达式的输出
返回类型由以下代码给出:
```cpp
decltype( (void)(std::declval().find(std::declval())), true)
```
让我们把它分解一下:
* `decltype`的操作数是一个逗号分隔的表达式列表。这意味着编译器将构造但不计算表达式,并使用最右边值的类型来确定函数的返回类型。
* `std::declval < T > ()`允许我们将 T 类型转换为引用类型,然后我们可以使用它来访问成员函数,而无需实际构造对象。
* 与所有基于 SFINAE 的操作一样,如果逗号分隔列表中的任何表达式无效,则该函数将被丢弃。如果它们都有效,则将其添加到函数列表中进行查找。
* 强制转换为 void 是为了防止用户重载逗号运算符时可能出现的任何问题。
* 基本上,这是在测试`C`类是否有一个名为`find()`的成员函数,该函数以`类 T`、`类 T &`或`const 类 T &`作为参数。
此方法适用于`std::set`,它有一个`find()`方法,该方法接受一个参数,但对于其他容器将失败,因为它们没有`find()`成员方法。
如果我们只处理一种类型,这种方法效果很好。但是如果我们有一个函数需要基于类型产生不同的实现,就像我们之前看到的那样,if constexpr 的方法要干净得多,通常也更容易理解。要使用`if constexpr`方法,我们需要在编译时生成评估为`true`或`false`的模板。标准库为此提供了助手类:`std::true_type`和`std::false_type`。这两个结构有一个静态常量成员名值,分别设置为`真`和`假`。使用 SFINAE 和模板重载,我们可以创建新的检测类,从这些类中的任何一个派生,以给出我们想要的结果:
```cpp
template
auto test_find(long) -> std::false_type;
template
auto test_find(int)
-> decltype(void(std::declval().find(std::declval())), std::true_type{});
template
struct has_find : decltype(test_find(0)) {};
```
第一个模板`test_find`创建默认行为,将返回类型设置为`std::false_type`。注意这个有一个`长`的参数类型。
第二个模板`test_find`创建了一个特化,用于测试一个类,该类有一个名为`find()`的成员函数,返回类型为`std::true_type`。请注意,这有一个参数类型`int`。
**具有 _find < T,A0 >** 模板通过从 **test_find()** 函数的返回类型中派生自身来工作。如果 T 类没有 **find()** 方法,则只生成 **std::false_type** 版本的 **test_find()** ,因此**有 _find < T,A0>:value**值将为 false,如果 constexpr() 可以在**中使用。**
有趣的部分发生在 T 类有`find()`方法的情况下,因为两个`test_find()`方法都是生成的。但是专用版本采用`int`类型的参数,而默认版本采用`long`类型的参数。当我们用零(0)来“调用”函数时,它将匹配专用版本并使用它。参数差异很重要,因为您不能让两个函数具有相同的参数类型,并且只有返回类型不同。如果要检查此行为,请将参数从 0 更改为 0L,以强制使用长版本。
## 类模板
到目前为止,我们只处理了函数模板。但是模板也可以用来为类提供蓝图。模板化类声明的一般结构如下:
```cpp
template
class MyClass {
// variables and methods that use T.
};
```
模板函数允许我们产生通用算法,而模板类允许我们产生通用数据类型及其相关行为。
当我们介绍标准模板库时,我们强调它包括容器的模板–`向量`、`德格`、`堆栈`等等。这些模板允许我们存储和管理任何我们想要的数据类型,但是仍然按照我们期望的方式运行。
### 练习 4:编写班级模板
计算科学中最常用的两种数据结构是堆栈和队列。两者目前在 STL 中都有实现。但是为了熟悉模板类,我们将编写一个可以用于任何类型的堆栈模板类。让我们开始吧:
1. 在 Eclipse 中打开**第 2B 课**项目,然后在**项目浏览器**中,展开**第 2B 课**,然后展开**练习 04** ,双击**练习 4.cpp** 在编辑器中打开本练习的文件。
2. 配置一个新的**启动配置**、**l2be xerce 4**,运行名称为**练习 4** 。
3. 另外,配置一个新的 C/C++ 单元运行配置 **L2BEx4Tests** ,以运行 **L2BEx4tests** 。设置**谷歌测试运行程序**。
4. Click on the **Run** option for the test, which we have to run for the first time:

###### 图 2B.32:堆栈的初始单元测试
5. Open **Stack.hpp** in the editor. You will find the following code:
```cpp
#pragma once
#include
#include
#define EXERCISE4_STEP 1
namespace acpp
{
template
class Stack
{
public:
private:
std::vector m_stack;
};
} // namespace acpp
```
模板定义首先要注意的是,它必须放在一个头文件中,这个头文件可以包含在我们需要重用它的地方。其次,我们使用了一个 pragma 指令(`#pragma 一次`),它告诉编译器,如果它再次遇到这个要#included 的文件,就不需要了。虽然不是标准的严格组成部分,但几乎所有现代 C++ 编译器都支持它。最后,请注意,出于本练习的目的,我们选择将项目存储在 STL 向量中。
6. 在编辑器中,在`堆栈`类的`公共`部分添加以下声明:
```cpp
bool empty() const
{
return m_stack.empty();
}
```
7. At the top of the file, change **EXERCISE4_STEP** to a value of **10**. Click on the **Run** button. The Exercise 4 tests should run and fail:

###### 图 2B.33:跳到失败的测试
8. 点击失败测试的名称,即**defaultconstructionnitsempty**。它将在右侧的消息部分显示失败的原因。双击消息。它将打开测试失败的文件并跳转到违规的行,如前面的截图所示。这个测试有一个错误。在测试中,我们期望堆栈是空的。但是,我们可以看到`空()`的报道是假的。
9. 将`断言 _ 假`更改为`断言 _ 真`并重新运行测试。这一次,它通过了,因为它在测试正确的事情。
10. 接下来我们要做的是添加一些类型别名,以便在接下来的几个方法中使用。在编辑器中,在`空()`方法的正上方添加以下行:
```cpp
using value_type = T;
using reference = value_type&;
using const_reference = const value_type&;
using size_type = std::size_t;
```
11. 点击**运行**按钮重新运行测试。他们应该通过。在做测试驱动开发时,口头禅是写一个小测试,看到它失败,然后写足够的代码让它通过。在这种情况下,我们实际测试了别名的定义是否正确,因为编译失败是测试失败的一种形式。我们现在准备添加推送功能。
12. 在编辑器中,通过在**空()**方法的正下方添加以下代码来更改 **Stack.hpp**
13. 在文件顶部,将`锻炼 4 _ 步骤`更改为`15`的值。点击**运行**按钮。我们现在有两个测试运行并通过。在 **StackTests.cpp** 中的新测试`pushontostknottempty`证明了该推送可以使堆栈不再为空。我们需要添加更多的方法来确保它已经完成了预期的工作。
14. 在编辑器中,通过在`push()`方法的正下方添加以下代码来更改 **Stack.hpp** ,并将`execute 4 _ STEP`更改为`16`的值:
```cpp
size_type size() const
{
return m_stack.size();
}
```
15. 点击**运行**按钮运行测试。现在应该有三个通过测试。
16. 在编辑器中,通过在`push()`方法的正下方添加以下代码来更改 **Stack.hpp** ,并将`execute 4 _ STEP`更改为`18`的值:
```cpp
void pop()
{
m_stack.pop_back();
}
```
17. 点击**运行**按钮运行测试。现在应该有四个通过测试。
18. 在编辑器中,通过在`pop()`方法的正下方添加以下代码来更改 **Stack.hpp** ,并将`execute 4 _ STEP`更改为值`20` :
```cpp
reference top()
{
m_stack.back();
}
const_reference top() const
{
m_stack.back();
}
```
19. 点击**运行**按钮运行测试。现在有五个通过测试,我们已经实现了一个堆栈。
20. 从启动配置下拉菜单中,选择 **L2BExercise4** 并点击**运行**按钮。练习 4 将运行并生成类似于以下输出的内容:

###### 图 2B.34:练习 4 输出
检查现在在 **Stack.hpp** 文件中的代码。在类内部定义类型的方法在整个 STL 中很常见(尽管由于它们的传统,它们可能会使用 typedef)。`std::stack`模板接受两个参数,第二个参数定义要使用的容器——vector 可能是第一个。检查 **StackTests.cpp** 中的测试。测试应该被命名,以表明他们的目标是测试什么,他们应该专注于这样做。
### 活动 1:开发通用“包含”模板函数
编程语言 Python 有一个名为“in”的成员操作符,可以用于任何序列,即列表、序列、集合、字符串等。即使 C++ 有 100 多种算法,它也没有一种等效的方法来实现同样的功能。C++ 20 在`std::set`上引入了`contains()`方法,但这还不够。我们需要创建一个`contains()`模板函数,它与`std::set`、`std::string`、`std::vector`以及任何其他提供迭代器的容器一起工作。这是由在其上调用 end()的能力决定的。我们的目标是获得最佳性能,因此我们将在任何有成员方法的容器上调用`find()`成员方法(这将是最有效的),否则我们将返回到在容器上使用`std::end()`。我们还需要区别对待`std::string()`,因为它的`find()`方法返回一个特殊值。
我们可以使用一个通用模板和两个专门化来实现这一点,但是这个活动是使用 SFINAE 和 if constexpr 的技术来实现的。另外,这个模板必须只在支持`end(C)`的类上工作。按照以下步骤实施本活动:
1. 从**第 2B 课/练习 01** 文件夹加载准备好的项目。
2. 使用`npos`成员定义助手模板函数和类来检测标准:字符串大小写。
3. 定义辅助模板函数和类,检测该类是否有`find()`方法。
4. 定义 contains template 函数,该函数使用 constexpr 在三种实现中进行选择-字符串大小写、has find 方法或一般大小写。
执行上述步骤后,预期输出应该如下所示:

###### 图 2B.35:成功实现 contains 的 suc 输出
#### 注意
这项活动的解决方案可以在第 653 页找到。
## 总结
在这一章中,我们学习了接口、继承和多态性,这些扩展了我们对类型的处理技巧。我们第一次尝试使用 C++ 模板进行泛型编程,并接触了该语言从 C++ 标准库中免费提供给我们的东西,其中包括 STL。我们探索了 C++ 的一个刚刚好用的特性,那就是模板类型推演,使用模板的时候让我们的生活变得更加轻松。然后,我们进一步学习了模板,并学习了如何使用 SFINAE 和 if constexpr 来控制编译器包含的模板部分。这些构成了我们进入 C++ 之旅的基石。在下一章中,我们将重新访问堆栈和堆,并了解什么是异常、发生了什么以及何时发生。我们还将学习如何在异常发生时保护我们的程序免受资源损失。
================================================
FILE: docs/adv-cpp/04.md
================================================
# 四、不允许泄漏——异常和资源
## 学习目标
本章结束时,您将能够:
* 开发类来管理资源
* 开发异常健壮的代码,这样资源就不会通过 RAII 泄漏
* 实现可以通过移动语义转移资源所有权的类
* 实现控制隐式转换的类
在本章中,您将学习如何使用类来管理资源、防止泄漏以及防止复制大量数据。
## 简介
在*章 2A* 、*不允许鸭子-类型、演绎*中,我们简单的接触了一些概念,比如智能指针和移动语义。在本章中,我们将进一步探讨它们。事实证明,这些主题与资源管理和编写健壮的代码(能够经常长时间运行而没有问题的代码)密切相关。
为了理解会发生什么,我们将探索变量在内存中的位置,以及当它们超出范围时会发生什么。
我们将研究编译器为我们输入的内容生成什么样的汇编代码,并且我们将探索当异常发生时所有这些是如何受到影响的。
### 可变范围和寿命
在*章 2B* 、*不允许鸭子-模板和演绎*中,我们讨论了可变范围和生存期。让我们快速浏览一下它们的不同类型:
**范围**:
* **本地范围**(也称为**块范围**):这适用于在花括号(`{}`)内的块内声明的任何内容。
* **全局/文件范围**:这适用于在正常函数或类之外声明的变量,也适用于正常函数。
**寿命**:
* **自动生存期**:这里,局部变量在声明时创建,在退出所在范围时销毁。这些由堆栈管理。
* **永久寿命**:这里,全局和静态局部变量都有永久寿命。
* **动态寿命**:这里,变量是在程序员的要求下创建和销毁的(使用`新增的`和`删除操作符`)。这些变量从堆中分配内存。
我们将使用以下程序来弄清楚`局部变量`的行为——那些具有`自动寿命`和那些具有`动态寿命`的变量:

###### 图 3.1:可变范围和寿命的测试程序
当我们运行前面的程序时,会生成以下输出:

###### 图 3.2:寿命测试程序的输出
前面输出中的十六进制数(`0xnnnnnn`)是正在构造或析构的 Int 对象的地址。我们的程序从进入`第 46 行`开始,使用`主()`功能。在这一点上,程序已经做了大量的初始化,以便我们可以随时使用所有的东西。下图是两个堆栈–**电脑堆栈**和**数据堆栈**。
这些都是帮助我们解释幕后发生的事情的抽象概念。`PC 栈` ( `程序计数器栈`)用于记住程序计数器的值(一个指向需要运行的下一条指令的寄存器),而`数据栈保存`我们正在处理的值或地址。虽然这是两个独立的堆栈,但在真正的 CPU 上,它很可能作为一个堆栈来实现。让我们看看下面的表格,其中我们使用了缩写`OLn`来表示来自前面程序输出的行号:

###### 图 3.3:测试程序执行的详细分析(第 1 部分)
下面是测试程序执行的详细分析的第二部分:

###### 图 3.4:测试程序执行的详细分析(第 2 部分)
下面是测试程序执行的详细分析的第三部分:

###### 图 3.5:测试程序执行的详细分析(第 3 部分)
从这个简单的程序中,我们了解到一些重要的事实:
* 当我们通过值传递时,复制构造函数被调用(就像我们在这个例子中做的那样)。
* 返回一个类型只会导致调用一个构造函数(而不是两个构造函数——一个用于创建返回对象,一个用于存储返回数据的变量)——c++ 调用这个**复制省略**,现在在标准中是强制的。
* 在终止作用域时(结束的花括号“`}`),任何超出作用域的变量都会被调用析构函数。如果这是真的,那么为什么地址`0x6000004d0`没有显示析构函数调用(`~Int()`)?这就引出了下一个事实。
* **原始指针**的析构函数只“破坏”指针,而不是它所指向的对象。这意味着当我们退出`计算()`方法时,我们泄漏了一些内存。
当我们忘记释放资源时,后两个事实对于理解和解决资源泄漏问题非常重要。我们将在处理完 C++ 中的异常后再来看资源管理。
## c++ 中的异常
我们已经看到了 C++ 如何用自动和动态的生存期来管理局部范围变量。当变量超出范围时,它会调用具有自动生存期的析构函数。我们还看到了原始指针在超出范围时是如何被破坏的。因为它没有清理动态生存期变量,所以我们丢失了它们。这是故事的一部分,将我们带向**资源获取是初始化** ( **RAII** )以后。但是,首先,我们需要了解异常如何改变程序的流程。
### 异常的必要性
在*章节 2A* 、*不允许鸭子–类型和演绎*中,我们被介绍到枚举,作为一种处理神奇数字的方式,用于`check_file()`功能:
```cpp
FileCheckStatus check_file(const char* name)
{
FILE* fptr{fopen(name,"r")};
if ( fptr == nullptr)
return FileCheckStatus::NotFound;
char buffer[30];
auto numberRead = fread(buffer, 1, 30, fptr);
fclose(fptr);
if (numberRead != 30)
return FileCheckStatus::IncorrectSize;
if(is_valid(buffer))
return FileCheckStatus::InvalidContents;
return FileCheckStatus::Good;
}
```
上述功能使用称为**状态**或**错误代码**的技术来报告操作结果。这是用于 C 风格编程的方法,其中处理与 **POSIX API** 和 **Windows API** 相关的错误。
#### 注意
`POSIX`代表`便携式操作系统界面`。它是一个 IEEE 标准,用于 Unix 和其他操作系统之间的软件兼容性。
这意味着,方法的调用方必须检查返回值,并对每种错误类型采取适当的操作。当您可以推理出代码将生成的错误类型时,这种方法很有效。这并不总是正确的。例如,提供给程序的数据可能有问题。这导致程序中出现无法处理的异常状态。代码中具有处理错误逻辑的部分将从检测到问题的代码部分中删除。
虽然可以编写处理这种问题的代码,但它增加了处理所有错误情况的复杂性,从而使程序难以阅读,难以推理函数应该做什么,因此非常难以维护。
对于错误处理,与错误代码相比,异常具有以下优势:
* 错误代码可以忽略–异常会强制处理错误(或者程序终止)。
* 异常可以沿着堆栈向上流动,到达响应错误的最佳方法。错误代码需要从每个中间方法传播出去。
* 异常将错误的处理从主程序流中分离出来,从而使软件易于可读性和可维护性。
* 异常将检测错误的代码与处理错误的代码分开。
如果您遵循最佳实践并针对异常情况使用异常,则使用异常不会产生(时间)开销。这是因为一个实现良好的编译器会传递 C++ 的咒语——你不用为你不用的东西付费。这可能会消耗一些内存,您的代码可能会稍微大一点,但运行时间应该不会受到影响。
C++ 使用异常来处理运行时异常。通过使用异常,我们可以检测到一个错误,抛出一个异常,然后错误传播回可以处理它的位置。我们修改一下之前的程序,引入`divide()`函数,改变 calculate()函数来调用。我们还将在`main()`函数中添加日志记录,这样我们就可以探索异常的行为:

###### 图 3.6:用于调查异常的修改后的测试程序
当我们编译并运行前面的程序时,会生成以下输出:

###### 图 3.7:测试程序的输出
在前面的代码中,您可以看到注释被添加到右侧。现在,我们删除程序中`结果 2`行的注释,重新编译程序,然后重新运行它。生成的新输出如下所示:

###### 图 3.8:测试程序的输出–结果 2
通过比较输出,我们可以看到每一行的前八行是相同的。前面输出的下两行相加是因为`divide()`函数被调用了两次。最后一行表示引发了异常,程序被终止。
对`divide()`函数的第二次调用试图除以零,这是一个异常操作。这导致了一个异常。如果一个整数被零除,就会导致浮点异常。这与异常在`POSIX`系统中生成的方式有关——它使用一种叫做信号的东西(我们在这里不讨论信号的细节)。当一个整数被零除时,`POSIX`系统将其映射到名为 **SIGFPE** 的信号,该信号最初是用于`浮点错误`的,但现在是更通用的`算术错误`。
#### **注**
根据 C++ 标准,如果零作为“`/`”运算符(除)或“`%`”运算符(模)的除数出现,则行为未定义。大多数系统会选择抛出异常。
因此,我们从前面的解释中学到了一个重要的事实:一个未处理的异常将终止程序(在内部,它调用`std::terminate()`)。我们将修复`未定义的行为`,捕捉异常,并查看输出中的变化。要修复`未定义行为`,我们需要在文件顶部添加`# include`,修改`divide()`功能:
```cpp
Int divide(Int a, Int b )
{
if (b.m_value == 0)
throw std::domain_error("divide by zero error!");
return a.m_value/b.m_value;
}
```
当我们重新编译并运行程序时,我们会得到以下输出:

###### 图 3.9:当我们抛出异常时的输出
从前面的输出中我们可以看出,变化不大。只是我们没有得到一个`浮点异常`(核心被转储)——程序仍然终止,但是没有转储核心。然后我们在`main()`函数中添加了一个`try/catch`块,以确保异常不再被处理。

###### 图 3.10:捕捉异常
重新编译程序并运行它,以获得以下输出:

###### 图 3.11:捕获异常的程序的输出
在前面的输出中,在注释为“**copy a for call divide**”的第二行抛出异常。此后输出的所有内容都是正在处理的异常的结果。
我们的代码已经将程序控制转移到了`main()`函数中的`catch()`语句,并且已经为堆栈上构造的所有变量执行了析构函数(从在`try`子句中进行调用时开始)。
### 堆叠展开
C++ 语言保证的销毁所有局部函数变量的过程称为**栈展开**。当堆栈在出现异常时展开时,C++ 使用其定义良好的规则来销毁范围内的所有对象。
当异常发生时,函数调用堆栈开始从当前函数线性搜索回调用它的函数,再到调用它的函数,以此类推,直到找到与异常匹配的异常处理程序(由`catch`块表示)。
如果发现异常处理程序,那么堆栈展开发生,破坏堆栈中所有函数的所有局部变量。对象按照与创建时相反的顺序销毁。如果没有找到处理抛出异常的处理程序,那么程序终止(通常不警告用户)。
### 练习 1:在分数和堆栈中实现异常
在本练习中,我们将返回到我们在*章节 2A* 、*不允许鸭子–类型和演绎*和*章节 2B* 、*不允许鸭子–模板和演绎*–`分数`和`堆栈`中学习的两个类,这两个类都可能经历运行时异常。我们将更新他们的代码,以便他们可以在检测到任何问题时引发异常。按照以下步骤实施本练习:
1. 打开 Eclipse,使用在**第 3 课**示例文件夹中找到的文件创建一个名为**第 3 课**的新项目。
2. 由于这是一个基于 **CMake 的项目**,将当前的构建器改为 **CMake Build(可移植)**。
3. 进入**项目** | **构建全部**菜单构建所有练习。默认情况下,屏幕底部的控制台将显示 **CMake 控制台【第 3 课】**。
4. 配置新的**启动配置**、**L3 练习 1** 以名称**练习 1** 运行。
5. 另外,配置一个新的 C/C++ 单元运行配置 **L3Ex1Tests** ,运行 **L3Ex1tests** 。设置**谷歌测试运行程序**。
6. Click on the **Run** option for the existing **18** tests to run and pass.

###### 图 3.12:现有测试全部通过(运行:18)
7. 在编辑器中打开 **Fraction.hpp** ,将文件顶部的行改为这样:
```cpp
#define EXERCISE1_STEP 14
```
8. Click on the **Run** button to re-run the tests – we have added one test that will attempt to create a `Fraction` with a zero denominator. The test expects that an exception has been thrown:

###### 图 3.13:新的失败测试
9. 点击失败的测试名称–现在**消息**窗口将显示预期行为和实际行为。您可能需要向右滚动才能全部阅读。在最右边,它表示“`预期……抛出一个 std::domain_error 类型的异常`”,下一行表示“`实际:它什么都不抛出`”。
10. Double-click on the message and it will take you to the following test:

###### 图 3.14:失败的测试
`ASSERT_THROW()`宏需要两个参数。由于`分数初始值设定项`中有一个逗号,因此有必要将整个第一个参数包装在一组额外的括号中。第二个参数将从这个构造函数中得到一个`std::domain_error`。内部`try/catch`结构存在,以确认预期的字符串在异常对象内部被捕获。如果我们不想检查这个,那么我们可以这样简单地编写测试:
```cpp
ASSERT_THROW(({Fraction f1{1,0}; }), std::domain_error);
```
11. 在编辑器中打开文件 **Fraction.cpp** 。在文件顶部附近插入以下行:
```cpp
#include
```
12. 修改构造函数,如果创建时分母为零,则抛出异常:
```cpp
Fraction::Fraction(int numerator, int denominator)
: m_numerator{numerator}, m_denominator{denominator}
{
if(m_denominator == 0)
{
throw std::domain_error("Zero Denominator");
}
}
```
13. 点击**运行**按钮重新运行测试。 **19** 测试现在通过。
14. 在编辑器中打开 **Fraction.hpp** ,将文件顶部附近的行改为这样:
```cpp
#define EXERCISE1_STEP 20
```
15. 点击**运行**按钮重新运行测试-新测试**失败。**
16. 点击失败的测试名称–现在**消息**窗口将显示预期行为和实际行为。您可能需要向右滚动才能全部阅读。在最右边,它表示“`预期……抛出一个类型为 std::runtime_error`的异常”,下一行表示“`实际:它抛出一个不同的类型`”。
17. Double-click on the message again to open the failing test:

###### 图 3.15:另一个失败的测试
该测试正在验证除法赋值运算符是否会为被零除抛出异常。
18. 打开**分数. cpp** 并定位`操作员/=()`功能。你会看到,在这个函数里面,它实际上使用了**分数**的构造函数,所以它会抛出一个`std::domain_error`。
19. 现在修改`运算符/=()`以在调用构造函数之前检测这个问题,这样它就会抛出一个带有预期消息的`std::runtime_error`。
20. 通过添加一个域错误来修改**分数. cpp** ,该域错误将检测除法运算符:
```cpp
Fraction& Fraction::operator/=(const Fraction& rhs)
{
if (rhs.m_numerator == 0)
{
throw std::runtime_error("Fraction Divide By Zero");
}
Fraction tmp(m_numerator*rhs.m_denominator,
m_denominator*rhs.m_numerator);
*this = tmp;
return *this;
}
```
21. 点击**运行**按钮重新运行测试。所有 **20** 测试通过。
22. 在编辑器中打开 **Stack.hpp** ,将文件顶部附近的行改为如下所示:
```cpp
#define EXERCISE1_STEP 27
```
23. Click on the **Run** button to re-run the tests – we have added one test that will attempt to pop from an empty stack. In the **C/C++ Unit tab** window, click on the triangle next to `FractionTest` to collapse the lists of tests and show the `StackTest`:

###### 图 3.16:弹出堆栈测试失败
24. 使用 **C/C++ 单元**窗口点击并定位失败的测试。确定预期异常,然后打开 **Stack.hpp** 。在文件顶部添加`# include`,然后更新`pop()`功能,使其看起来像这样:
```cpp
void pop()
{
if(empty())
throw std::underflow_error("Pop from empty stack");
m_stack.pop_back();
}
```
25. 点击**运行**按钮重新运行测试。 **21** 测试现在通过。
26. 在编辑器中打开 **Stack.hpp** ,将文件顶部的行改为如下所示:
```cpp
#define EXERCISE1_STEP 31
```
27. 点击**运行**按钮重新运行测试-新增加的测试**失败。**
28. 使用 **C/C++ 单元**窗口点击并定位失败的测试。确定预期异常,然后打开 **Stack.hpp** 。更新非常数`top()`方法,使其看起来如下:
```cpp
reference top()
{
if(empty())
throw std::underflow_error("Top from empty stack");
return m_stack.back();
}
```
29. 点击**运行**按钮重新运行测试。 **22** 测试通过。
30. 在编辑器中打开 **Stack.hpp** ,将文件顶部的行改为如下所示:
```cpp
#define EXERCISE1_STEP 35
```
31. 点击**运行**按钮重新运行测试-新增加的测试**失败。**
32. 使用 **C/C++ 单元**窗口点击并定位失败的测试。确定预期异常,然后打开 **Stack.hpp** 。更新常量`top()`方法,使其看起来如下:
```cpp
const_reference top() const
{
if(empty())
throw std::underflow_error("Top from empty stack");
return m_stack.back();
}
```
33. 点击**运行**按钮重新运行测试。所有 **23** 测试现在通过。
在本练习中,我们添加了运行时检查预条件,这是使用我们的`分数`和`堆栈`类的正常操作的一部分。这段代码只会在违反一个先决条件时抛出异常,表明数据或我们的程序执行方式有问题。
### 抛出异常时会发生什么?
在某个时刻,我们的程序执行以下语句:
```cpp
throw expression;
```
通过执行这个操作,我们发出了一个信号,表明出现了一个错误的情况,并且我们希望它得到处理。接下来发生的事情是一个**临时**对象,被称为**异常对象**,它被构建在一个未指定的存储中,并根据表达式进行复制初始化(它可以调用移动构造函数,并可能会被复制省略)。异常对象的类型是从表达式中静态确定的,删除了 const 和 volatile 限定符。数组类型衰减为指针,而函数类型转换为函数的指针。如果表达式的类型是格式错误的或抽象的,那么将发生编译器错误。
构造异常对象后,控件连同异常对象一起被转移到异常处理程序。所选择的异常处理程序是堆栈展开时与异常对象具有最接近匹配类型的处理程序。异常对象存在,直到最后一个 catch 子句退出,除非它被重新抛出。表达式的类型必须有一个可访问的`复制构造函数`和一个`析构函数`。
### 按值投掷或按指针投掷
知道了一个临时异常对象被创建,传递,然后销毁,抛出表达式应该使用什么类型?一个`值`还是一个`指针`?
我们还没有过多讨论在 catch 语句中指定类型。我们很快就会这么做。但是现在,请注意,要捕获指针类型(已经抛出),捕获模式也需要是指针类型。
如果一个指向对象的指针被抛出,那么抛出方必须确保异常对象将指向的对象(因为它将是指针的副本)将保持活动状态,直到异常被处理,甚至通过`栈展开`。
指针可以指向静态变量、全局变量或从堆中分配的内存,以确保在处理异常时被指向的对象仍然存在。现在,我们已经解决了保持异常对象活动的问题。但是当处理者处理完它之后,捕手会怎么处理它呢?
异常的捕捉者不知道异常对象的创建(`全局`、`静态`或`堆`)因此不知道是否应该删除接收到的指针。因此,按指针抛出不是抛出异常的推荐方法。
抛出的对象将被复制到创建的临时异常对象,并传递给处理程序。当异常被处理后,临时对象将被销毁,程序将继续运行。关于如何处理它,没有任何含糊之处。因此,最好的做法是通过值抛出**异常。**
### 标准库异常
C++ 标准库将`标准::异常`定义为所有标准库异常的基类。该标准定义了以下第一级层次的`异常` / `错误`(括号中的数字表示有多少个异常源自该类):

###### 图 3.17: 标准库异常层次结构(两级)
这些异常通过包括 STL 的 C++ 标准库来使用。创建自己的异常类的最佳实践是从一个标准异常中派生它。正如我们接下来将看到的,您的特殊异常可以被一个标准异常的处理程序捕获。
### 捕捉异常
在讨论异常的必要性时,我们引入了抛出异常的想法,但并没有真正考虑 C++ 如何支持捕捉异常。异常处理的过程从一段代码被包装在`try`块中开始,将其置于**异常检查**下。try 块后面是一个或多个 catch 块,它们是异常处理程序。当在 try 块内执行代码时出现异常情况时,将引发异常,并将控制权转移给异常处理程序。如果没有抛出异常,则跳过所有异常处理程序,try 块中的代码完成,正常执行继续。让我们在代码片段中表达这些概念:
```cpp
void SomeFunction()
{
try {
// code under exception inspection
}
catch(myexception e) // first handler – catch by value
{
// some error handling steps
}
catch(std::exception* e) // second handler – catch by pointer
{
// some other error handling steps
}
catch(std::runtime_error& e) // third handler – catch by reference
{
// some other error handling steps
}
catch(...) // default exception handler – catch any exception
{
// some other error handling steps
}
// Normal programming continues from here
}
```
前面的片段显示了必要的关键词–`尝试`,以及`捕捉`,并介绍了三种不同类型的捕捉模式(不包括默认处理程序):
* **按值捕获异常**:这是一种代价高昂的机制,因为异常处理程序的处理与任何其他函数一样。按值捕获意味着必须创建异常对象的副本,然后将其传递给处理程序。第二个副本的创建减慢了异常处理过程。这种类型也可能受到对象切片的影响,其中抛出了一个子类,catch 子句是一个超类。catch 子句将只接收丢失原始异常对象属性的超类对象的副本。因此,我们应该避免按值捕获异常处理程序。
* **通过指针捕捉异常**:正如在查看按值抛出时所讨论的,使用按指针抛出,这种风格的异常处理程序只能捕捉指针抛出的异常。因为我们只想按值抛出,所以我们应该避免使用指针捕捉异常处理程序。
* **Catch expression by reference**: This is the recommended style of exception handler as it does not suffer from the issues related to catch-by-value and catch-by-pointer. As a reference is passed to the handler, no second copy of the exception object is made. Splicing does not occur because the reference still refers to the originally thrown exception object. And since the exception was thrown by value, the temporary exception object will be destroyed automatically when we are done with it.
#### 注意
处理异常时,是`按值抛出`、`按引用捕捉`。
当有多个 catch 块时,异常对象类型用于按照指定的顺序匹配处理程序。一旦找到匹配的处理程序,它就会被执行,其余的异常处理程序将被忽略。这与函数解析不同,在函数解析中,编译器会找到与参数的最佳匹配。因此,异常处理程序(catch 块)应该从更具体的到更一般的来定义。例如,默认处理程序(`catch(...)`)应该总是排在定义的最后。
### 练习 2:实现异常处理程序
在本练习中,我们将实现异常处理程序的层次结构,以管理如何处理异常。按照以下步骤实施本练习:
1. Open the **Lesson3** project in Eclipse. Then in the **Project Explorer**, expand **Lesson3** then **Exercise02** and double click on **exceptions.cpp** to open the file for this exercise into the editor. This file contains the following code:
```cpp
#include
#include
void run_exceptions()
{
try
{
throw std::domain_error("We got one!!!!");
}
catch(...)
{
std::cout << "Exception caught by default handler\n";
}
catch(const std::exception& e)
{
std::cout << "Exception '" << "' caught by std::exception handler\n";
}
catch(const std::logic_error& e)
{
std::cout << "Exception '" << "' caught by std::logic_error handler\n";
}
catch(const std::domain_error& e)
{
std::cout << "Exception '" << "' caught by std::domain_error handler\n";
}
}
int main()
{
std::cout << "\n\n------ Exercise 2 ------\n";
run_exceptions();
std::cout << "Complete.\n";
return 0;
}
```
#### **注**
所有异常处理程序都使用了相同的名称作为异常参数,即`e`。该变量的作用域只是声明它的 catch 块。
2. 点击**启动配置**下拉菜单,选择**新启动配置……**。从**搜索项目**菜单中配置**L3 练习 2** 应用,以名称**L3 练习 2** 运行它。
3. 完成后,将是当前选择的**启动配置**。
4. Click on the **Run** button. Exercise 2 will run and produce the following output:

###### 图 3.18:练习 2 输出-默认处理程序捕获了异常
5. 在控制台窗口中,点击**显示选中的控制台**按钮,选择 **CDT 全局构建控制台**。滚动窗口。您会发现(如果使用 GCC 编译器的话)有五条警告消息与我们放置异常处理程序的顺序有关。(实际上,第一个警告通常是一个错误,除了`CMake`文件在编译该目标时设置了`-fpermissive`标志。)
6. In the editor, move the default exception handler, `catch(...)`, to just after the `std::domain_error` handler. Click on the **Run** button. Exercise 2 will run and produce the following output:

###### 图 3.19:使用了标准::异常处理程序
7. 在编辑器中,将`std::exception`处理程序移到`std::domain_error`处理程序之后。点击**运行**按钮。这一次,它将报告执行了`std::logic_error`处理程序。
8. 在编辑器中,将`std:: logic_error`处理程序移到`std::domain_error`处理程序之后。点击**运行**按钮。这一次,它将报告执行了`std:: domain_error`处理程序,这实际上是我们所期望的。
9. 现在将`掷`线改为`std::logic_error`异常。点击**运行**按钮。这一次,它将报告`std::logic_error`处理程序按预期执行。
10. 现在将`抛出`线改为`标准::下溢 _ 错误`异常。点击**运行**按钮,这一次它将报告异常被`std::异常`处理程序捕获,正如预期的那样。`std::exception`是所有标准库异常的基类。
在本练习中,我们实现了一系列异常处理程序,并观察了异常处理程序的顺序如何影响捕获异常的方式以及如何使用异常层次结构。
### CMake 生成器表达式
使用`CMake`时,有时需要调整变量值。`CMake`是一个构建生成器系统,可以为很多构建工具和编译器工具链生成构建文件。由于这种灵活性,如果您想在编译器中打开某些功能,您只需要将它应用于一种特定的类型。这是因为不同供应商的命令行选项不同。例如,g++ 编译器启用 C++ 17 支持的命令行选项是`-std=c++ 17`,但对于`msvc`则是`/std:c++ 17`。如果打开 **CMakeLists.txt** 文件,定位**l3 锻炼 2**`add _ executable`,那么后面会有一行:
```cpp
target_compile_options(L3Exercise2 PRIVATE $<$:-fpermissive>)
```
这使用`$`变量查询来检查是否是 GCC 编译器。如果是,则生成 1(真),否则生成 0(假)。它还使用`$ <条件:true_string >`条件表达式将`-fppermissive`添加到**l3 锻炼 2** 目标的编译器选项中,但仅限于 gcc 编译器。这些可以作为对`target_compile_options`的单独调用或通过一次调用为每个编译器类型添加。
#### 注意
有关生成器表达式的更多信息,请查看以下链接:[https://cmake . org/cmake/help/v 3.15/manual/cmake-generator-expressions . 7 . html](https://cmake.org/cmake/help/v3.15/manual/cmake-generator-expressions.7.html)。
### 异常使用指南
在 C++ 代码中使用异常时,请记住以下几点:
* 吟诵:**按值抛投,按参考接球**
* **正常程序流程不要使用异常**。如果一个函数满足一个异常条件,并且不能满足它的(函数)义务,那么也只有这样,你才会抛出一个异常。如果该功能能够解决异常情况并履行其义务,那么它就不是异常。它们被命名为异常是有原因的,如果您不使用它们,您将不会产生任何处理开销。
* **不要从析构函数**中抛出异常。请记住,由于堆栈展开,将执行局部变量析构函数。如果在堆栈展开过程中调用析构函数并引发异常,程序将终止。
* **不要吞下异常**。不要使用默认的 catch 处理程序,也不要处理异常。引发异常是为了表明存在问题,您应该对此采取措施。忽略异常可能会导致稍后难以排除的故障。这是因为任何有用的信息都会随着被吞咽的异常而真正丢失。
* **异常对象从抛出**中复制。
## 资源管理(在异常的世界中)
到目前为止,我们已经了解了局部变量的作用域,以及当变量超出作用域时如何处理`自动`和`动态寿命变量`——自动寿命变量(那些放在堆栈上的)被完全析构,而`动态寿命变量`(那些被程序员分配到堆中的)没有被析构:我们只是失去了对它们的任何访问。我们还看到,当抛出异常时,会找到最近的匹配处理程序,在堆栈展开过程中,抛出点和处理程序之间的所有局部变量都会被析构。
我们可以利用这些知识来编写健壮的资源管理类,这将使我们不再需要跟踪资源(动态生存期变量、文件句柄、系统句柄等),以确保当我们使用完它们时,它们被释放(回到野外)。在正常运行和异常情况下,用于管理资源的技术被称为**资源获取是初始化** ( **RAII** )。
### 资源获取是初始化
RAII 是另一个命名不当的概念的好例子(另一个是`SFINAE`)。`RAII`或`资源获取是初始化`描述了用于管理资源的类的行为。如果把它命名为**破坏就是资源释放**可能会更好,它真正抓住了管理类试图做的事情的本质。我们可以从我们之前的讨论中推断出如何实现这一点,但是展示一个单独的例子来开发资源管理`文件`类,并展示 RAI 如何提高可读性和我们推理函数功能的能力,会更有启发性。
考虑以下代码:
```cpp
void do_something()
{
FILE* out{};
FILE* in = fopen("input.txt", "r");
try
{
if (in != nullptr)
{
// UNSAFE – an exception here will create a resource leak
out = fopen("output.txt", "w");
if (out != nullptr)
{
// Do some work
// UNSAFE – an exception here will create resource leaks
fclose(out);
}
fclose(in);
}
}
catch(std::exception& e)
{
// Respond to the exception
}
}
```
这段代码显示了资源管理的两个潜在问题:
* 最重要的是,在文件打开和关闭之间出现异常会导致资源泄漏。如果这是一个系统资源,其中许多会导致系统不稳定或应用性能受到不利影响,因为它缺乏资源。
* 此外,由于错误处理,在一个方法中管理多个资源会导致深嵌套子句。这不利于代码的可读性,因此也不利于代码的理解和可维护性。很容易忘记释放一个资源,尤其是有多个退出点的时候。
那么,我们如何管理资源,以便拥有异常安全和更简单的代码呢?这个问题并不是 C++ 独有的,不同的语言对它的管理也不同。`Java`、`C#`和`Python`使用垃圾收集方法,该方法会扫描创建的对象,并在它们不再被引用时进行清理。但是 C++ 没有垃圾收集,那么解决方案是什么呢?
考虑以下类别:
```cpp
class File {
public:
File(const char* name, const char* access) {
m_file = fopen(name, access);
if (m_file == nullptr) {
throw std::ios_base::failure("failed to open file");
}
}
~File() {
fclose(m_file);
}
operator FILE*() {
return m_file;
}
private:
FILE* m_file{};
};
```
此类实现以下特征:
* 构造函数获取资源。
* 如果构造函数中没有获取资源,则会引发异常。
* 当类被销毁时,资源被释放。
如果我们在`do_something()`方法中使用这个类,那么它看起来像这样:
```cpp
void do_something()
{
try
{
File in("input.txt", "r");
File out("output.txt", "w");
// Do some work
}
catch(std::exception& e)
{
// Respond to the exception
}
}
```
如果在这样做的时候发生异常,那么 C++ 保证所有基于堆栈的对象都将调用它们的析构函数(`堆栈展开`,从而保证文件被关闭。这解决了出现异常时资源泄漏的问题,因为资源现在已被自动清理。此外,这种方法非常容易阅读,这样我们就可以理解逻辑流程,而不必担心错误处理。
该技术利用`文件`对象的生存期来获取和释放资源,确保资源不泄露。资源在管理类的构建(初始化)过程中获取,在管理类的销毁过程中释放。正是这种受范围限制的资源的行为产生了名称`资源获取是初始化`。
前面的示例涉及管理作为系统资源的文件句柄。它适用于任何需要在使用前获得,然后在完成时放弃的资源。RAII 技术可以应用于广泛的资源——打开的文件、打开的管道、分配的堆内存、打开的套接字、执行的线程、数据库连接、互斥锁/关键部分的锁定——基本上是主机系统中供应不足且需要管理的任何资源。
### 练习 3:为内存和文件句柄实现 RAII
在本练习中,我们将实现两个不同的类,它们将使用 RAII 技术管理内存或文件。按照以下步骤实施本练习:
1. 在 Eclipse 中打开**第 3 课**项目。然后在**项目浏览器**中,展开**第 3 课**,然后展开**练习 03** ,双击**练习 3.cpp** 将本练习的文件打开到编辑器中。
2. 点击**启动配置**下拉菜单,选择**新启动配置……**。从“搜索项目”菜单中配置**L3 练习 3** 应用,以名称**L3 练习 3** 运行它。
3. Click on the **Run** button to run Exercise 3\. This will produce the following output:

###### 图 3.20:来自练习 3.cpp 的内存和文件泄漏
输出显示我们分配了五次内存,地址由 new 返回。当从`main()`函数执行时,当`监视器`被析构时,它转储已分配和释放的内存报告,以及已打开但从未关闭的文件。
4. 在编辑器中,在`文件`类的**练习 3.cpp** 文件中键入以下内容:
```cpp
class File {
public:
File(const char* name, const char* access) {
m_file = fopen(name, access);
if (m_file == nullptr) {
throw std::ios_base::failure(""failed to open file"");
}
}
~File() {
fclose(m_file);
}
operator FILE*() {
return m_file;
}
private:
FILE* m_file{};
};
```
5. 点击**运行**按钮运行练习 3–它仍然会泄漏文件和内存,但是代码是正确的。
6. 找到`泄漏文件()`函数,并对其进行修改,使其使用新的`文件`类(与前面的代码类似)来防止文件泄漏:
```cpp
void LeakFiles()
{
File fh1{"HelloB1.txt", "w"};
fprintf(fh1, "Hello B2\n");
File fh2{"HelloB2.txt", "w"};
fprintf(fh2, "Hello B1\n");
}
```
7. Click on the **Run** button to run Exercise 3\. If you have modified `LeakFiles()` correctly, then the output will be as follows:

###### 图 3.21:没有文件泄漏
8. 现在在**练习 3.cpp** 中,添加以下`CharPointer`类:
```cpp
class CharPointer
{
public:
void allocate(size_t size)
{
m_memory = new char[size];
}
operator char*() { return m_memory;}
private:
char* m_memory{};
};
```
9. 将`泄漏指针()`修改如下:
```cpp
void LeakPointers()
{
CharPointer memory[5];
for (auto i{0} ; i<5 ; i++)
{
memory[i].allocate(20);
std::cout << "allocated 20 bytes @ " << (void *)memory[i] << "\n";
}
}
```
10. 点击**运行**按钮运行练习 3–仍然有内存泄漏,但是代码是正确的。
11. 现在,添加以下析构函数到`字符指针`。注意`删除`运算符使用数组`[]`语法:
```cpp
~CharPointer()
{
delete [] m_memory;
}
```
12. 再次点击**运行**按钮运行练习 3–这一次,您应该看到监视器没有报告泄漏:

###### 图 3.22:无泄漏-内存或文件
`文件`和`CharPointer`的实现提供了`RAII`设计方法,但是在设计这些时还有其他的考虑。例如,我们需要复制构造函数还是复制赋值函数?在这两种情况下,仅仅将资源从一个对象复制到另一个对象可能是一个问题,因为这可能导致两次尝试关闭文件句柄或删除内存。通常,这将导致未定义的行为。接下来,我们将根据资源管理对象的实现重新访问特殊成员函数,如`文件`或`CharPointer`。
### 特殊编码技术
*练习 3* 、*为内存和文件句柄*实现 RAII 的代码是专门编写的,这样我们可以监控内存和文件句柄的使用情况,并在退出时报告任何泄漏。访问 **monitor.h** 和 **monitor.cpp** 文件,检查用于使监视器成为可能的两种技术:
* **Preprocessor macros**: This is the special use of a preprocessor macro to demonstrate the leaks and should not be used in production code, that is, replacing a function by text substitution.
如果您使用视窗应用编程接口编程,您可能偶尔会发现您的方法名称与微软用于其应用编程接口方法的宏冲突。例如,如果您包含 **windows.h** ,请不要调用您的任何方法`发送消息`。如果您这样做了,那么根据您是构建 ASCII 模式还是 Unicode 模式,它将分别被`发送消息`或`发送消息`替换。
* **定义我们自己的新处理程序**:这是一种先进的技术,除非你编写嵌入式代码,否则你不太可能需要它。
### C++ 最终不需要
支持异常抛出机制的其他语言(`C#`、`Java`和`可视化 Basic.NET`)有一个`try/catch/finally`范例,其中`finally`块中的代码在从 try 块退出时被调用——正常或异常。C++ 没有`最后`块,因为它可以访问更好的机制,确保我们不会忘记释放一个资源——RAII。由于资源由本地对象表示,本地对象的析构函数将释放资源。
这种设计模式的额外优势是,如果正在管理大量资源,那么`最后`块将按比例变大。RAII 消除了对 finally 的需求,并导致更容易维护的代码。
### RAII 和 STL
标准模板库(STL)在其许多模板和类中使用了 RAI。例如,在 C++ 11 中引入的智能指针,即`std::unique_ptr`和`std::shared_ptr`,通过确保在内存用完时释放内存,或者确保在其他地方使用内存时不释放内存,来帮助避免许多问题。STL 中的其他示例包括`标准::字符串`(内存)、`标准::向量`(内存)和`标准::流`(文件句柄)。
### 这个物体是谁的?
使用前面的`文件`和`字符指针`的实现,我们已经用 RAII 测试了资源管理。让我们进一步探讨它。首先,我们将定义一个不止有一个资源的类:
```cpp
class BufferedWriter
{
public:
BufferedWriter(const char* filename);
~BufferedWriter();
bool write(const char* data, size_t length);
private:
const size_t BufferSize{4096};
FILE* m_file{nullptr};
size_t m_writePos{0};
char* m_buffer{new char[BufferSize]};
};
```
该类用于缓冲对文件的写入。
#### 注意
当使用 iostream 派生类时,这通常不是必需的,因为它们已经提供了缓冲。
对`write()`函数的每次调用都会将数据添加到分配的缓冲区中,直到到达`缓冲区`,此时数据实际上被写入文件,缓冲区被重置。
但是如果我们想把这个`BufferedWriter`的实例分配给另一个实例或者复制它呢?什么是正确的行为?
如果我们只是让默认的复制构造函数/复制赋值做它们该做的事情,我们会得到一个成员方式的项目副本。这意味着我们有两个`BufferedWriter`的实例,它们持有相同的文件句柄和指向缓冲区的指针。当对象的第一个实例被销毁时,作为优秀的程序员,我们将通过关闭文件来清理文件,通过删除文件来清理内存。第二个实例现在有一个失效的文件句柄和一个指向内存的指针,我们已经告诉操作系统为下一个用户恢复。任何使用这些资源的尝试,包括销毁它们,都将导致未定义的行为,并且很可能导致程序崩溃。默认的复制构造函数/复制赋值操作符执行所谓的浅复制——也就是说,它一点一点地复制所有成员(但不是它们所引用的)。
我们拥有的两种资源可以区别对待。首先,应该只有一个类拥有`m_buffer`。处理这个问题有两种选择:
* 防止类的复制,从而防止内存的复制
* 执行`深度复制`,其中第二个实例中的缓冲区已由构造函数分配,第一个缓冲区的内容被复制
其次,应该只有一个类拥有文件句柄(`m_file`)。处理这个问题有两种选择:
* 防止类的复制,从而防止文件句柄的复制
* 将`所有权`从原实例转移到第二实例,并将原实例标记为无效或空(无论是什么意思)
实现深度复制很容易,但是我们如何转移资源的所有权呢?为了回答这个问题,我们需要再次查看临时对象和值类别。
### 临时对象
创建一个临时对象来存储表达式的中间结果,然后将结果存放到变量中(或者只是忘记)。表达式是任何返回值的代码,包括向函数传递值、从函数返回值、隐式转换、文本和二进制运算符。临时对象是`右值表达式`,它们有内存,临时为它们分配一个位置来放置表达式结果。在 C++ 11 之前,正是这种临时对象的创建和它们之间的数据复制导致了一些性能问题。为了解决这个问题,C++ 11 引入了`右值引用`来启用所谓的移动语义。
### 移动语义
一个`右值引用`(用一个双“与”符号表示,`& &`)是一个引用,它只被赋予一个`右值`,这个右值将延长右值的寿命,直到`右值引用`完成。所以,`值`可以超越定义它的表达式。借助`右值引用`,我们现在可以通过移动构造函数和移动赋值操作符实现移动语义。移动语义的目的是从被引用对象中窃取资源,从而避免昂贵的复制操作。移动完成后,被引用对象必须保持稳定状态。换句话说,被移动的对象必须保持这样一种状态,即当它被销毁时,不会导致任何未定义的行为或程序崩溃,也不会影响从它那里窃取的资源。
C++ 11 还引入了一个强制转换操作符`std::move()`,它将一个`左值`强制转换为一个`右值`,这样就可以调用移动构造函数或移动赋值操作符来“移动”资源。`std::move()`方法实际上并不移动数据。
需要注意的一件意想不到的事情是,在移动构造函数和移动赋值操作符中,`右值`引用实际上是一个`左值`。这意味着,如果您想确保移动语义发生在方法中,那么您可能需要在成员变量上再次使用`std::move()`。
随着 C++ 11 引入移动语义,它还更新了标准库,以利用这一新功能。例如,`std::string`和`std::vector`已经更新为包含移动语义。获得移动语义的好处;你只需要用最新的 C++ 编译器重新编译你的代码。
### 实现智能指针
智能指针是一个资源管理类,它持有指向资源的指针,并在超出范围时释放它。在本节中,我们将实现一个智能指针,观察它作为复制支持类的行为,将其演化为支持移动语义,并最终移除它对复制操作的支持:
```cpp
#include
template
class smart_ptr
{
public:
smart_ptr(T* ptr = nullptr) :m_ptr(ptr)
{
}
~smart_ptr()
{
delete m_ptr;
}
// Copy constructor --> Do deep copy
smart_ptr(const smart_ptr& a)
{
m_ptr = new T;
*m_ptr = *a.m_ptr; // use operator=() to do deep copy
}
// Copy assignment --> Do deep copy
smart_ptr& operator=(const smart_ptr& a)
{
// Self-assignment detection
if (&a == this)
return *this;
// Release any resource we're holding
delete m_ptr;
// Copy the resource
m_ptr = new T;
*m_ptr = *a.m_ptr;
return *this;
}
T& operator*() const { return *m_ptr; }
T* operator->() const { return m_ptr; }
bool is_null() const { return m_ptr == nullptr; }
private:
T* m_ptr{nullptr};
};
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource released\n"; }
};
smart_ptr createResource()
{
smart_ptr res(new Resource); // Step 1
return res; // return value invokes the copy constructor // Step 2
}
int main()
{
smart_ptr the_res;
the_res = createResource(); // assignment invokes the copy assignment Step 3/4
return 0; // Step 5
}
```
当我们运行这个程序时,会生成以下输出:

###### 图 3.23:智能指针程序输出
对于这样一个简单的程序,有大量的资源获取和释放。让我们把这个分开:
1. `createResource()`内部的局部变量 res 在堆上创建并初始化(动态生存期),导致第一条“`资源获取了`”消息。
2. 编译器可以创建另一个临时来返回值。但是,编译器已经执行了`复制省略`来移除副本(也就是说,它能够将对象直接构建到调用函数分配的堆栈位置上)。编译器有`返回值优化` ( `RVO`)和`命名返回值优化` ( `NRVO`)可以应用的优化,在 C++ 17 下,这些在某些情况下是强制性的。
3. 通过复制分配,临时对象被分配给 **main()** 函数中的 _res 变量。由于拷贝分配正在进行深度拷贝,因此会获取资源的另一个拷贝。
4. 当分配完成时,临时对象超出范围,我们得到第一个“资源释放”消息。
5. 当`main()`函数返回时,`的 _res`超出范围,释放第二个资源。
因此,如果资源很大,我们在`main()`中创建`RES`局部变量的方法效率非常低,因为我们在大块内存中创建和复制,因为复制分配中有深度复制。但是我们知道,当`createResource()`创建的临时变量不再需要时,那么我们就要扔掉它,释放它的资源。在这些场景中,将资源从临时实例转移(或移动)到该类型的其他实例会更有效。移动语义使得重写我们的`smart_ptr`模板成为可能,以便不进行深度复制而是转移资源。
让我们给我们的`smart_ptr`类添加移动语义:
```cpp
// Move constructor --> transfer resource
smart_ptr(smart_ptr&& a) : m_ptr(a.m_ptr)
{
a.m_ptr = nullptr; // Put into safe state
}
// Move assignment --> transfer resource
smart_ptr& operator=(smart_ptr&& a)
{
// Self-assignment detection
if (&a == this)
return *this;
// Release any resource we're holding
delete m_ptr;
// Transfer the resource
m_ptr = a.m_ptr;
a.m_ptr = nullptr; // Put into safe state
return *this;
}
```
重新运行程序后,我们得到以下输出:

###### 图 3.24:使用移动语义的智能指针程序输出
现在,因为移动赋值现在可用,编译器在这一行使用它:
```cpp
the_res = createResource(); // assignment invokes the copy assignment Step 3/4
```
`第 3 步`现在被移动分配取代,这意味着深度副本现在已经被移除。
`步骤 4`不再释放资源,因为带有注释“//”的行进入安全状态——它不再有资源可以释放,因为它的所有权被转移了。
关于`移动构造函数`和`移动赋值`需要注意的另一点是,在它们的拷贝版本中参数是常量的地方,它们在它们的移动版本中是`非常量`。这被称为`所有权转移`,这意味着我们需要修改传入的参数。
移动构造函数的另一种实现可能如下所示:
```cpp
// Move constructor --> transfer resource
smart_ptr(smart_ptr&& a)
{
std::swap(this->m_ptr, a.m_ptr);
}
```
本质上,我们是在交换资源,C++ STL 支持将交换作为具有许多专门化的模板。这是因为我们使用成员初始化将`m_ptr`设置为`nullptr`。因此,我们正在用存储在`a`中的值交换一个`nullptr`。
既然我们已经修复了不必要的深度复制问题,我们实际上可以从`smart_ptr()`中删除复制操作,因为所有权的转移实际上是我们想要的。如果我们将一个非临时的`smart_ptr`的实例复制到另一个非临时的`smart_ptr`的实例,那么我们将有两个对象,当它们超出范围时将删除资源,这不是期望的行为。为了删除(深度)复制操作,我们更改了成员函数的定义,如下所示:
```cpp
smart_ptr(const smart_ptr& a) = delete;
smart_ptr& operator=(const smart_ptr& a) = delete;
```
`= delete`的后缀,我们在*章节【2A】*、*不允许鸭子-类型和演绎*中看到,告诉编译器试图访问具有该原型的函数现在不是有效代码,并导致错误。
### STL 智能指针
STL 提供了我们可以用来在对象上实现 RAI 的类,而不是必须编写自己的`smart_ptr`。原版本是`std::auto_ptr()`,在 C++ 11 中被弃用,在 C++ 17 中被删除。它是在`右值`引用支持之前创建的,由于它使用复制实现了移动语义而导致了问题。C++ 11 引入了三个新模板来管理资源的生存期和所有权:
* **std::unique_ptr** :通过指针拥有并管理一个`单个对象`,当`unique_ptr`超出范围时销毁该对象。它有两个版本:用于单个对象(使用`新建`创建)和用于对象数组(使用`新建【】`创建)。`unique_ptr`和直接使用底层指针一样高效。
* **std::shared_ptr** :通过指针保留对象的共享所有权。它通过使用引用计数器来管理资源。分配给 shared_ptr 的 shared_ptr 的每个副本都会更新引用计数。当引用计数变为零时,这意味着没有所有者了,资源被释放/销毁。
* **std::weak_ptr** :提供与`shared_ptr`相同资源的接口,但不修改计数器。可以检查资源是否仍然存在,但不会阻止资源被销毁。如果您确定该资源仍然存在,则可以使用它来获取该资源的`shared_ptr`。它的一个用例是多个`shared_ptrs`以循环引用结束的场景。循环引用会阻止资源的自动释放。`weak_ptr`用于打破循环,允许在应该释放资源的时候释放资源。
### std::unique_ptr
`std::unique_ptr()`是在 C++ 11 中引入的,用来代替`std::auto_ptr()`并为我们提供了`smart_ptr`所做的一切(以及更多)。我们可以重新编写我们的`smart_ptr`程序如下:
```cpp
#include
#include
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource released\n"; }
};
std::unique_ptr createResource()
{
std::unique_ptr res(new Resource);
return res;
}
int main()
{
std::unique_ptr the_res;
the_res = createResource(); // assignment invokes the copy assignment
return 0;
}
```
我们可以更进一步,因为 C++ 14 引入了一个助手方法,在处理`unique_ptrs`时保证异常安全:
```cpp
std::unique_ptr createResource()
{
return std::make_unique();
}
```
*为什么有这个必要?*考虑以下函数调用:
```cpp
some_function(std::unique_ptr(new T), std::unique_ptr(new U));
```
这样做的问题是,编译器可以自由地按照它喜欢的任何顺序对参数列表中的操作序列进行排序。它可以调用`新 T`,然后`新 U`,然后`STD::unique _ ptr()`,最后`STD::unique _ ptr()`。这个序列的问题是,如果`新 U`抛出异常,那么调用`新 T`分配的资源没有被放入`unique_ptr`中,不会被自动清理。`STD::make _ unique<>()`的使用保证了调用的顺序,使得资源的构造和`unique_ptr`的构造一起发生,不会泄露资源。在 C++ 17 中,对`make_unique`的需求已经被移除,在这种情况下,围绕评估顺序的规则已经被收紧。然而,使用`make _ unique()`方法可能仍然是一个好主意,因为将来任何到共享 ptr 的转换都将更容易。
名称`unique_ptr`明确了模板的意图,即它是它所指向的对象的唯一所有者。这在`auto_ptr`中并不明显。同样地,`shared_ptr`也很明确,它打算共享资源。`唯一 _ptr`模板提供对以下操作员的访问:
* **T* get()** :返回托管资源的指针。
* **运算符 bool()** :如果实例管理资源,则返回`true`。(`get()!= nullptr`)。
* **T &运算符*(T1):**左值**对托管资源的引用。与 ***get()** 相同。**
* **T*运算符- > ()** :指向托管资源的指针。与`获得()`相同。
* **T &运算符[](size_t index)** :对于`unique_ptr(new [])`,它提供对托管阵列的访问,就像它本身是一个阵列一样。返回一个`左值`引用,以便设置和获取该值。
### std::shared_ptr
当您想要共享资源的所有权时,会使用共享指针。你为什么要这么做?有几个场景非常适合资源共享,例如在图形用户界面程序中,您可能希望共享字体对象、位图对象等等。 **GoF 飞行重量设计模式**将是另一个例子。
`std::shared_ptr`提供了与`std::unique_ptr`相同的功能,但是开销更大,因为它现在必须跟踪对象的引用计数。所有为`std::unique_ptr`描述的操作符都可以在`std::shared_ptr`上使用。一个区别是创建`std::shared_ptr`的推荐方法是调用`STD::make _ shared<>()`。
在编写库或工厂时,库的作者并不总是知道用户想要如何使用已经创建的对象,因此建议从您的工厂方法中返回`unique_ptr < T >`。这样做的原因是用户可以通过赋值轻松地将`std::unique_ptr`转换为`STD::shared _ ptr`;
```cpp
std::unique_ptr unique_obj = std::make_unique();
std::shared_ptr shared_obj = unique_obj;
```
这将转移所有权,并使`unique_obj`为空。
#### 注意
一旦资源成为共享资源,它就不能被还原成唯一的对象。
### std::weak_ptr
弱指针是共享指针的变体,但它不包含对资源的引用计数。所以,这并不妨碍它在计数归零时被释放。考虑以下程序结构,它可能出现在正常的图形用户界面中:
```cpp
#include
#include
struct ScrollBar;
struct TextWindow;
struct Panel
{
~Panel() {
std::cout << "--Panel destroyed\n";
}
void setScroll(const std::shared_ptr sb) {
m_scrollbar = sb;
}
void setText(const std::shared_ptr tw) {
m_text = tw;
}
std::weak_ptr m_scrollbar;
std::shared_ptr m_text;
};
struct ScrollBar
{
~ScrollBar() {
std::cout << "--ScrollBar destroyed\n";
}
void setPanel(const std::shared_ptr panel) {
m_panel=panel;
}
std::shared_ptr m_panel;
};
struct TextWindow
{
~TextWindow() {
std::cout << "--TextWindow destroyed\n";
}
void setPanel(const std::shared_ptr panel) {
m_panel=panel;
}
std::shared_ptr m_panel;
};
void run_app()
{
std::shared_ptr panel = std::make_shared();
std::shared_ptr scrollbar = std::make_shared();
std::shared_ptr textwindow = std::make_shared();
scrollbar->setPanel(panel);
textwindow->setPanel(panel);
panel->setScroll(scrollbar);
panel->setText(textwindow);
}
int main()
{
std::cout << "Starting app\n";
run_app();
std::cout << "Exited app\n";
return 0;
}
```
执行时,它输出以下内容:

###### 图 3.25:弱指针程序输出
这表明当应用退出时,面板和`文本窗口`没有被破坏。这是因为他们彼此都持有`共享 _ptr`,因此两者的参考计数不会归零并触发销毁。如果我们用图解法描述这个结构,那么我们可以看到它有一个`共享 _ptr`循环:

###### 图 3.26:弱 _ptr 和共享 _ptr 周期
### 智能指针和调用函数
既然我们可以管理我们的资源,我们如何使用它们?我们传递聪明的指针吗?当我们有一个智能指针(`unique_ptr`或`shared_ptr`时,我们在调用函数时有四个选项:
* 按值传递智能指针
* 通过引用传递智能指针
* 通过指针传递托管资源
* 通过引用传递托管资源
这不是一份详尽的清单,但却是需要考虑的主要清单。如何传递智能指针或其资源的答案取决于我们调用函数的意图:
* 函数的意图是仅仅使用资源吗?
* 该函数是否拥有资源的所有权?
* 该函数是否替换托管对象?
如果函数只是去`使用资源`,那么它甚至不需要知道它正在被交给一个托管资源。它只需要使用它,并且应该通过指针使用资源来调用,或者通过引用使用资源(或者甚至通过值使用资源):
```cpp
do_something(Resource* resource);
do_something(Resource& resource);
do_something(Resource resource);
```
如果您想将资源的所有权**传递给函数,那么该函数应该由智能指针通过值来调用,并使用 **std::move()** 来调用:**
```cpp
do_something(std::unique_ptr resource);
auto res = std::make_unique();
do_something (std::move(res));
```
当`do _ 某物()`返回时,`res`变量将为空,资源现在归`do _ 某物()`所有。
如果你想`替换被管理对象`(一个称为**重新拔插**的过程),那么你通过引用传递智能指针:
```cpp
do_something(std::unique_ptr& resource);
```
下面的程序将所有这些放在一起演示每个场景以及如何调用函数:
```cpp
#include
#include
#include
#include
class Resource
{
public:
Resource() { std::cout << "+++ Resource acquired ["<< m_id <<"]\n"; }
~Resource() { std::cout << "---Resource released ["<< m_id <<"]\n"; }
std::string name() const {
std::ostringstream ss;
ss << "the resource [" << m_id <<"]";
return ss.str();
}
int m_id{++ m_count};
static int m_count;
};
int Resource::m_count{0};
void use_resource(Resource& res)
{
std::cout << "Enter use_resource\n";
std::cout << "...using " << res.name() << "\n";
std::cout << "Exit use_resource\n";
}
void take_ownership(std::unique_ptr res)
{
std::cout << "Enter take_ownership\n";
if (res)
std::cout << "...taken " << res->name() << "\n";
std::cout << "Exit take_ownership\n";
}
void reseat(std::unique_ptr& res)
{
std::cout << "Enter reseat\n";
res.reset(new Resource);
if (res)
std::cout << "...reseated " << res->name() << "\n";
std::cout << "Exit reseat\n";
}
int main()
{
std::cout << "Starting...\n";
auto res = std::make_unique();
// Use - pass resource by reference
use_resource(*res);
if (res)
std::cout << "We HAVE the resource " << res->name() << "\n\n";
else
std::cout << "We have LOST the resource\n\n";
// Pass ownership - pass smart pointer by value
take_ownership(std::move(res));
if (res)
std::cout << "We HAVE the resource " << res->name() << "\n\n";
else
std::cout << "We have LOST the resource\n\n";
// Replace (reseat) resource - pass smart pointer by reference
reseat(res);
if (res)
std::cout << "We HAVE the resource " << res->name() << "\n\n";
else
std::cout << "We have LOST the resource\n\n";
std::cout << "Exiting...\n";
return 0;
}
```
当我们运行这个程序时,我们会收到以下输出:

###### 图 3.27:所有权传递程序输出
#### 注意
*C++ 核心指南*有一整节关于*资源管理*,智能指针,以及如何使用它们这里:[http://isocpp . github . io/cppcoreiders/cppcoreiders # S-resource](http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#S-resource)。我们只触及了准则所涵盖的最重要的方面。
### 练习 4:用 STL 智能指针实现 RAII
在本练习中,我们将实现一个传感器工厂方法,通过`unique_ptr`返回传感器资源。我们将实现一个`unique_ptr`来保存一个数组,然后开发代码将一个`unique_ptr`转换成一个共享指针,然后再共享一些。按照以下步骤实施本练习:
1. 在 Eclipse 中打开**第 3 课**项目。然后在**项目浏览器**中,展开**第 3 课**,然后展开**练习 04** ,双击**练习 4.cpp** 将本练习的文件打开到编辑器中。
2. 点击**启动配置**下拉菜单,选择**新启动配置……**。从**搜索项目**菜单中配置**L3 练习 4** 应用,使其以名称**L3 练习 4** 运行。
3. Click on the **Run** button to run Exercise 4\. This will produce the following output:

###### 图 3.28:练习 4 的输出
4. In the editor, examine the code, particularly the factory method, that is, `createSensor(type)`.
```cpp
std::unique_ptr
createSensor(SensorType type)
{
std::unique_ptr sensor;
if (type == SensorType::Light)
{
sensor.reset(new LightSensor);
}
else if (type == SensorType::Temperature)
{
sensor.reset(new TemperatureSensor);
}
else if (type == SensorType::Pressure)
{
sensor.reset(new PressureSensor);
}
return sensor;
}
```
这将创建一个名为传感器的空的唯一指针,然后根据传入的`类型`用所需的传感器重置包含的指针。
5. 在编辑器中打开练习 4.cpp,将文件顶部附近的行改为如下所示:
```cpp
#define EXERCISE4_STEP 5
```
6. Click on the **Run** button to compile the code, which will fail with the following error:

###### 图 3.29:步骤 5 的编译器错误
完整的错误消息如下:
```cpp
error: conversion from 'std::unique_ptr' to non-scalar type 'SensorSPtr {aka std::shared_ptr}' requested
```
根据错误,我们试图将`唯一 _ptr`分配给`共享 _ptr`,这是不允许的。
7. 找到报告错误的行,并将其改为如下内容:
```cpp
SensorSPtr light2 = std::move(light);
```
8. Click on the **Run** button to compile and run the program. The output is as follows:

###### 图 3.30:练习 4 的成功输出(练习 4_STEP = 5 之后)
前面的输出显示,我们创建了三个不同的传感器,光传感器指针从持有资源到移动,并且**光 2** 共享指针有两个所有者。等等!什么事?两个主人?但是我们所做的只是将资源从`light`(一个`unique_ptr`)移动到`light2`(一个`shared_ptr`)。问题实际上是模板方法:
```cpp
template
void printSharedPointer(SP sp, const char* message)
```
第一个参数是通过值传递的,这意味着将创建一个新的`shared_ptr`副本并传递给方法进行打印。
9. Let's fix that now by changing the template to pass-by-reference. Click on the **Run** button to compile and run the program. The following output is generated:

###### 图 3.31:已更正的 printSharedPointer 输出
10. 在编辑器中打开**练习 4.cpp** ,将文件顶部附近的行改为这样:
```cpp
#define EXERCISE4_STEP 12
```
11. Click on the **Run** button to compile and run the program. The following output is generated:

###### 图 3.32:练习 4 的注释步骤 12 输出
12. 将输出与`测试传感器()`方法中的代码进行比较。我们会发现我们可以很容易地分配给一个空的`unique_ptr` ( `light`)并且我们可以从一个`shared_ptr`分配给另一个(`light3 = light2`)而不需要`std::move()`。
13. 在编辑器中打开**练习 4.cpp** ,将文件顶部附近的行改为这样:
```cpp
#define EXERCISE4_STEP 15
```
14. Click on the **Run** button to compile and run the program. The output switches to the following:

###### 图 3.33:在 unique_ptr 中管理阵列
15. Open the editor and find the `testArrays()` method:
```cpp
void testArrays()
{
std::unique_ptr board = std::make_unique(8*8);
for(int i=0 ; i<8 ; i++)
for(int j=0 ; j<8 ; j++)
board[i*8+j] = 10*(i+1)+j+1;
for(int i=0 ; i<8 ; i++)
{
char sep{' '};
for(int j=0 ; j<8 ; j++)
std::cout << board[i*8+j] << sep;
std::cout << "\n";
}
}
```
这段代码中有几点需要注意。首先,类型被声明为 **int[]** 。我们选择了 **int** 进行本练习,但它可以是任何类型。其次,当**unique _ ptr**(c++ 17 中的 **shared_ptr** )用于管理数组时,定义**运算符[]** 。因此,我们通过从二维索引的板[i*8+j] 计算一维索引来模拟二维数组。
16. 编辑方法第一行,声明`自动`类型:
```cpp
auto board = std::make_unique(8*8);
```
17. 点击**运行**按钮编译并运行程序——输出将与前一次运行相同。在这种情况下,auto 非常有用,因为您不再需要在类型声明中键入所有细节,也不再需要调用`make_unique()`。
在本练习中,我们实现了一个工厂功能,该功能使用`unique_ptr`来管理传感器的寿命,从而为制造的传感器提供服务。然后我们实现了代码,将它从一个`unique_ptr`更改为几个对象。最后,我们开发了一种`独特的 _ptr`技术来使用一维数组管理多维数组。
### 零/五法则——不同的视角
当我们引入 **BufferedWriter** 时,它有两个被管理的资源:内存和一个文件。然后,我们讨论了默认编译器如何生成被称为浅拷贝的拷贝操作。我们讨论了如何以不同的方式管理资源—停止拷贝、执行深度拷贝或转移所有权。我们在这种情况下决定做的事情被称为资源管理策略。你选择的政策会影响你如何执行零/五的**规则。**
在资源管理方面,一个类可以不管理任何资源,管理一个可以复制但不能移动的资源,管理一个可以移动但不能复制的资源,或者管理一个既不能复制也不能移动的资源。下列类别显示了如何表达这些内容:
```cpp
struct NoResourceToManage
{
// use compiler generated copy & move constructors and operators
};
struct CopyOnlyResource
{
~CopyOnlyResource() {/* defined */ }
CopyOnlyResource(const CopyOnlyResource& rhs) {/* defined */ }
CopyOnlyResource& operator=(const CopyOnlyResource& rhs) {/* defined */ }
CopyOnlyResource(CopyOnlyResource&& rhs) = delete;
CopyOnlyResource& operator=(CopyOnlyResource&& rhs) = delete;
};
struct MoveOnlyResource
{
~MoveOnlyResource() {/* defined */ }
MoveOnlyResource(const MoveOnlyResource& rhs) = delete;
MoveOnlyResource& operator=(const MoveOnlyResource& rhs) = delete;
MoveOnlyResource(MoveOnlyResource&& rhs) {/* defined */ }
MoveOnlyResource& operator=(MoveOnlyResource&& rhs) {/* defined */ }
};
struct NoMoveOrCopyResource
{
~NoMoveOrCopyResource() {/* defined */ }
NoMoveOrCopyResource(const NoMoveOrCopyResource& rhs) = delete;
NoMoveOrCopyResource& operator=(const NoMoveOrCopyResource& rhs) = delete;
NoMoveOrCopyResource(NoMoveOrCopyResource&& rhs) = delete;
NoMoveOrCopyResource& operator=(NoMoveOrCopyResource&& rhs) = delete;
};
```
由于在所有上下文和异常情况下管理资源的复杂性,最佳实践是,如果一个类负责管理资源,那么该类只负责管理该资源。
### 活动 1:用 RAII 和 Move 实现图形处理
在*章 2A**不准鸭子入内-类型与演绎*中,你的团队努力工作,得到了`点 3d`和`矩阵 3d`的实施。现在,您的公司想要营销该库,在他们做到这一点之前,它需要两大改进:
* 这些类必须在我们公司的命名空间中,即高级 Plus Inc .中。因此,图形的命名空间将是`accp::gfx`。
* `点 3d`和`矩阵 3d`中矩阵的存储是类的固有部分,因此它是从堆栈而不是堆中分配的。作为库矩阵支持的发展,我们需要从堆中分配内存。当我们致力于在未来的版本中实现更大的矩阵时,我们也希望在我们的类中引入移动语义。
按照以下步骤实现:
1. 从我们当前版本的库开始(可以在**第 3 课/练习 01** 文件夹中找到),将我们所有的类放入`acpp::gfx`命名空间。
2. 修复所有因为变更而失败的测试。(失败可能意味着编译失败,而不仅仅是运行测试。)
3. 在`Matrix3d`中,从直接在类中声明矩阵切换到堆分配的内存进行存储。
4. 通过实现复制构造函数和复制赋值操作符的深度复制实现来修复失败的测试。进行任何其他必要的更改,以适应新的内部表示。请注意,您不需要修改任何测试来让它们通过,它们只访问公共接口,这意味着我们可以在不影响客户端的情况下更改内部结构。
5. 通过在返回语句中使用`std::move`在`CreateTranslationMatrix()`中强制调用移动构造函数来触发另一个失败。在`Matrix3d`类中介绍所需的移动操作,以使测试能够编译并通过。
6. 对`点 3d`重复步骤 3 至 4。
在执行了前面的步骤之后,预期的输出从一开始就不会改变:

###### 图 3.34:成功转换为使用 RAII 后的活动 1 输出
#### 注意
这个活动的解决方案可以在第 657 页找到。
### 什么时候调用函数?
C++ 程序执行的所有操作本质上都是函数调用(尽管编译器可能会将它们优化为内联操作序列)。然而,由于**语法糖**,你正在进行函数调用可能并不明显。语法糖是编程语言中的语法,它使阅读或表达变得更容易。比如你写`a = 2 + 5`的时候,本质上是在调用`运算符=( & a,运算符+(2,5))`。只是这种语言允许我们编写第一种形式,但第二种形式允许我们重载运算符,并将这些功能扩展到用户定义的类型。
以下机制导致对函数的调用:
* 对函数的显式调用。
* 所有运算符,如+、-、*、/、%等,以及 new/delete。
* 变量声明–如果存在初始化值,将导致用参数调用构造函数。
* 用户定义的文字–我们没有处理这些,但是本质上,我们为`类型运算符“【名称(参数)`”定义了一个重载。然后我们可以编写诸如 10_km 这样的东西,这使得我们的代码更容易理解,因为它携带了语义信息。
* 从一个值到另一个值的铸造(`静态 _ 铸造< >`,`const _ 铸造< >`,`重新解释 _ 铸造< >`,以及`动态 _ 铸造< >`)。同样,我们还有另一个运算符重载,它允许我们从一种类型转换为另一种类型。
* 在函数重载期间,可能需要将一种类型转换为另一种类型,以便它与函数原型相匹配。它可以通过调用具有正确参数类型的构造函数来创建临时的,或者通过隐式调用的强制转换操作符来实现。
编译器中的每一个结果都决定了一个函数必须被调用。在确定需要调用函数后,它必须找到与名称和参数匹配的函数。这是我们将在下一节讨论的内容。
### 调用哪个函数
在*章节 2A**不允许鸭子-类型和演绎*中,我们看到功能过载解析执行如下:

###### 图 3.35:函数霸王解析
我们真正没有深入研究的是名称查找的概念。在某个时候,编译器会遇到对`函数`函数的以下调用:
```cpp
func(a, b);
```
当这种情况发生时,它必须将其名称与引入它的声明相关联。这个过程叫做**名称查找**。对于程序中的所有项目(变量、名称空间、类、函数、函数模板和模板),这种名称查找是正确的。对于要编译的程序,变量、名称空间和类的名称查找过程必须生成一个声明。但是,对于函数和函数模板,编译器可能会将多个具有相同名称的声明关联起来——主要是通过函数重载,由于**依赖于参数的查找** ( **ADL** ),函数重载可能会被扩展以考虑其他函数。
### 标识符
按照 C++ 标准的定义,**标识符**是由大写和小写拉丁字母、数字、下划线和大多数 Unicode 字符组成的序列。有效的标识符必须以非数字字符开头,并且长度任意且区分大小写。每个角色都很重要。
### 名称
**名称**用于指代实体或标签。名称是以下形式之一:
* 标识符
* 函数符号中的重载运算符名称(例如运算符-,运算符删除)
* 模板名称后跟其参数列表(向量)
* 用户定义的转换函数名(运算符 float)
* 用户定义的文字运算符名称(运算符" " _ms)
每个实体及其名称都是通过声明引入的,而标签的名称是通过**转到**语句或通过带标签的语句引入的。一个名称可以在一个文件(或翻译单元)中多次使用,以根据范围引用不同的实体。根据链接的不同,一个名称也可以用来指代多个文件(翻译单元)中的同一个实体,或者不同的实体。编译器使用名称查找通过**名称查找**将引入名称的声明与程序中的未知名称相关联。
### 名称查找
名称查找过程是两个过程之一,根据上下文进行选择:
* **限定名查找**:名称出现在范围解析运算符`::`的右侧,或者可能出现在`:`之后,后跟`模板`关键字。限定名可以指命名空间成员、类成员或枚举数。`::`运算符左侧的名称定义了查找名称的范围。如果没有名称,则使用全局命名空间。
* **不合格名称查找**:其他一切。在这种情况下,名称查找检查当前范围和所有封闭范围。
如果未限定的名称留在函数调用运算符“`)(`”中,则它使用依赖于参数的查找。
### 依赖于参数的查找
查找非限定函数名的规则集被称为`依赖于参数的查找`(称为 ADL),或`柯尼格查找`(以安德鲁·克尼格命名,他定义了它,并且是 C++ 标准委员会的长期成员)。非限定函数名可以作为函数调用表达式出现,也可以作为对重载运算符的隐式函数调用的一部分出现。
ADL 基本上说,除了在非限定名称查找时考虑的范围和命名空间之外,还考虑了所有参数和模板参数的“关联命名空间”。考虑以下代码:
```cpp
#include
#include
int main()
{
std::string welcome{"Hello there"};
std::cout << welcome;
endl(std::cout);
}
```
当我们编译并运行这段代码时,输出如预期的那样:
```cpp
$ ./adl.exe
Hello there
$
```
这是一种不同寻常的编写程序的方法。通常,它会这样写:
```cpp
#include
#include
int main()
{
std::string welcome{"Hello there"};
std::cout << welcome << std::endl;
}
```
我们在用调用`endl()`的奇怪方法来展示 ADL。但是这里有两个 ADL 查找。
第一个经历 ADL 的函数调用是`std::cout < < welcome`,编译器认为是`运算符< < (std::cout,welcome)`。名称操作符< <现在可以在可用的范围及其参数的名称空间中找到–`标准`。这个额外的命名空间将名称解析为自由方法,即在字符串头中声明的`STD::operator<<(ostream&OS,string & s)`。
第二个调用更明显`endl(std::cout)`。同样,编译器可以访问 std 名称空间来解析这个名称查找,并在标题`中找到 **std::endl**`**模板**(包含在`iostream`中)。
没有 ADL,编译器无法找到这两个函数,因为它们是由 iostream 和 string 包提供给我们的自由函数。插入符的魔力(
namespace mylib
{
void is_substring(std::string superstring, std::string substring)
{
std::cout << "mylib::is_substring()\n";
}
void contains(std::string superstring, const char* substring) {
is_substring(superstring, substring);
}
}
int main() {
mylib::contains("Really long reference", "included");
}
```
当我们编译并运行前面的程序时,我们得到了预期的输出:

###### 图 3.36: ADL 示例程序输出
C++ 标准委员会随后决定引入一个`is_substring()`函数,如下所示:
```cpp
namespace std {
void is_substring(std::string superstring, const char* substring)
{
std::cout << "std::is_substring()\n";
}
}
```
如果我们将它添加到文件的顶部,编译并重新运行它,我们现在会得到以下输出:

###### 图 3.37: ADL 发布程序输出
得益于 ADL,(下一个 C++ 标准)编译器选择了不同的实现,更适合`is_substring()`的非限定函数调用。因为参数的隐式转换,所以不会发生冲突,这种冲突会导致歧义和编译器错误。它只是默默地采用新的方法,如果参数顺序不同,这可能会导致微妙和难以发现的错误。编译器只能检测类型和语法差异,而不能检测语义差异。
#### 注意
为了演示 ADL 是如何工作的,我们将我们的函数添加到了 std 名称空间中。名称空间用于分离关注点,并添加到其他人的名称空间中,特别是`标准库名称空间` ( `std`)不是好的做法。
那么,为什么要买者自负(买家当心)?如果您在开发中使用第三方库(包括 C++ 标准库),那么当您升级库时,您需要确保对接口的更改不会因为 ADL 而给您带来问题。
### 练习 5:实施模板以防止日常生活能力问题
在本练习中,我们将演示 C++ 17 STL 中的一个突破性变化,它可能会在野外引起一个问题。C++ 11 为`std::begin(type)`和朋友介绍了模板。作为开发人员,这是通用接口的一个吸引人的表达,您可能已经为 size(类型)和 empty(类型)编写了自己的版本。按照以下步骤实施本练习:
1. 在 Eclipse 中打开**第 3 课**项目。然后在**项目浏览器**中,展开**第 3 课**,然后展开**练习 05** ,双击**练习 5.cpp** 将本练习的文件打开到编辑器中。
2. 点击**启动配置**下拉菜单,选择**新启动配置……**。从“搜索项目”菜单中配置**L3 练习 5** 应用,使其以名称**L3 练习 5** 运行。
3. Click on the **Run** button to run Exercise 5\. This will produce the following output:

###### 图 3:38:成功执行练习 5
4. 对代码的检查揭示了两个助手模板:
```cpp
template
bool empty(const T& x)
{
return x.empty();
}
template
int size(const T& x)
{
return x.size();
}
```
5. 与所有其他练习不同,本练习被配置为在 C++ 14 下构建。打开**第 3 课**下的 **CMakeLists.txt** 文件,找到以下行:
```cpp
set_property(TARGET L3Exercise5 PROPERTY CXX_STANDARD 14)
```
6. 将`14`改为`17`。
7. Click on the **Run** button to compile the exercise which now fails:

###### 图 3.39:在 C++ 17 下编译失败——模糊的函数调用
8. 因为`empty()`和`size()`模板的参数是一个 std::vector,所以 ADL 引入了这些模板新包含的 STL 版本,并破坏了我们的代码。
9. 在**练习 5.cpp** 文件中,找到产生错误的两次出现的`空()`和两次出现的`大小()`,并在它们之前插入两个冒号“`::`”(范围说明符)。
10. 点击**运行**按钮,编译并运行练习。它现在愉快地编译并再次运行,因为对`空()`和`大小()`函数的调用现在被限定了。我们可以同样指定`标准::`范围。
在本练习中,我们在全局命名空间中实现了两个模板函数,如果我们在 C++ 14 标准下编译程序,这两个模板函数可以很好地工作。然而,当我们在 C++ 17 下编译时,我们的实现崩溃了,因为 STL 库发生了变化,我们不得不改变我们的实现,以确保编译器找到并使用我们编写的模板。
### 隐式转换
在*图 3.36* 、*函数重载解析*中确定候选函数集时,编译器必须查看名称查找过程中找到的所有可用函数,并确定参数号和类型是否与调用点匹配。在确定类型是否匹配时,它还将检查所有可用的转换,以确定是否有从类型 T1 类型(传递的参数的类型)转换为 T2 类型(为函数参数指定的类型)的机制。如果它可以将所有参数从 T1 转换为 T2,那么它会将该函数添加到候选集。
这种从 T1 类型到 T2 类型的转换被称为**隐式转换**,当在不接受该类型但接受其他类型(T2)的表达式或上下文中使用 T1 类型时,就会发生这种转换。这发生在以下环境中:
* 当调用以 T2 为参数声明的函数时,T1 作为参数传递。
* T1 用作期望 T2 的运算符的操作数。
* T1 用于初始化 T2 的一个新对象(包括返回语句)。
* T1 用在`开关`语句中(在这种情况下,T2 是一个整数)。
* T1 用于 if 语句或 **do-while** 或 **while** 循环(其中 T2 是布尔)。
如果存在从 T1 到 2 的明确转换序列,那么程序将编译。内置类型之间的转换通常由通常的算术转换决定。
### 显式–防止隐式转换
隐式转换是一个很好的特性,它使得程序员能够表达他们的意图,而且它在大多数时候都是有效的。然而,编译器在没有程序员提供提示的情况下将一种类型转换成另一种类型的能力并不总是令人满意的。考虑以下小程序:
```cpp
#include
class Real
{
public:
Real(double value) : m_value{value} {}
operator float() {return m_value;}
float getValue() const {return m_value;}
private:
double m_value {0.0};
};
void test(bool result)
{
std::cout << std::boolalpha;
std::cout << "Test => " << result << "\n";
}
int main()
{
Real real{3.14159};
test(real);
if ( real )
{
std::cout << "true: " << real.getValue() << "\n";
}
else
{
std::cout << "false: " << real.getValue() << "\n";
}
}
```
当我们编译它并运行前面的程序时,我们会得到以下输出:

###### 图 3.40:隐式转换示例程序输出
嗯,这可能有点出乎意料,这编译并实际产生了一个输出。**实数**变量属于**实数**类型,它有一个要浮动的转换运算符–**运算符 float()** 。 **test()** 函数以一个 **bool** 作为参数,如果条件一定会导致一个 **bool** 。如果数值为零,编译器会将任何数值类型转换为值为 false 的**布尔**类型,如果数值不为零,则转换为 true。但是,如果这不是我们想要的行为,我们可以通过在函数声明前加上显式关键字来防止它。假设我们更改了行,它的内容如下:
```cpp
explicit operator float() {return m_value;}
```
如果我们现在试图编译它,我们会得到两个错误:

###### 图 3.41:编译错误,因为隐式转换被移除了。
这两种情况都与无法将实数类型转换为布尔值有关–首先,在调用点进行`测试()`,然后在 if 条件下进行。
现在,让我们引入一个 bool 转换运算符来解决这个问题。
```cpp
operator bool() {return m_value == 0.0;}
```
我们现在可以再次构建程序。我们将收到以下输出:

###### 图 3.42:引入 bool 运算符代替隐式转换
`布尔`值现在为假,而以前为真。这是因为浮点转换返回的值的隐式转换不是零,然后转换为 true。
从 C++ 11 开始,所有的构造函数(复制和移动构造函数除外)都被认为是转换构造函数。这意味着,如果它们不是用显式声明的,那么它们可用于隐式转换。同样,任何未声明为显式的转换运算符都可以用于隐式转换。
`C++ 核心指南`有两条与隐式转换相关的规则:
* **C.46** :默认情况下,将单参数构造函数声明为显式的
* **C.164** :避免隐式转换运算符
### 上下文转换
如果我们现在对我们的小程序做一个进一步的改变,我们可以进入所谓的上下文转换。让我们明确 bool 运算符,并尝试编译程序:
```cpp
explicit operator bool() {return m_value == 0.0;}
```
我们将收到以下输出:

###### 图 3.43:使用显式 bool 运算符编译错误
这次我们在调用`test()`的地方只有一个错误,但不是 if 条件。我们可以通过使用 C 风格的 case (bool)或 c++ `static _ cast(real)`(这是首选方法)来修复此错误。当我们添加强制转换时,程序会再次编译并运行。
那么,如果 bool 强制转换是显式的,那么 if 表达式的条件为什么不需要强制转换呢?
C++ 标准允许在某些上下文中使用`bool`类型,并且存在 bool 转换的声明(无论是否标记为显式)。如果发生这种情况,则允许隐式转换。这在上下文中被称为**转换为布尔**,并且可能发生在以下上下文中:
* `的条件(或控制表达)如果`、`而`、`为`
* 内置逻辑运算符的操作数:`!`(不是)`& &`(和)和`||`(或)
* 三进制(或条件)运算符的第一个操作数`?:`。
### 练习 6:隐式和显式转换
在本练习中,我们将尝试调用函数、隐式转换、防止它们以及启用它们。按照以下步骤实施本练习:
1. 在 Eclipse 中打开**第 3 课**项目。然后在**项目浏览器**中,依次展开**第 3 课**和**练习 06** ,双击**练习 6.cpp** 将本练习的文件打开到编辑器中。
2. 点击**启动配置**下拉菜单,选择**新启动配置……**。从**搜索项目**菜单中配置**l3 锻炼 6** 应用,使其以名称**l3 锻炼 6** 运行。
3. Click on the **Run** button to run Exercise 6\. This will produce the following output:

###### 图 3.44:练习 6 的默认输出
4. 在文本编辑器中,将`电压`的构造函数更改为`显式` :
```cpp
struct Voltage
{
explicit Voltage(float emf) : m_emf(emf)
{
}
float m_emf;
};
```
5. Click on the **Run** button to recompile the code – now we get the following error:

###### 图 3.45:整数到电压的转换失败
6. 从构造函数中移除显式,并将`计算`函数改为引用:
```cpp
void calculate(Voltage& v)
```
7. Click on the **Run** button to recompile the code – now, we get the following error:

###### 图 3.46:整数到电压&
同一行有我们之前运行的问题,但原因不同。所以,*隐式转换只适用于值类型*。
8. 注释掉产生错误的行,然后在调用`后,使用 _float(42)`,添加以下行:
```cpp
use_float(volts);
```
9. Click on the **Run** button to recompile the code – now we get the following error:

###### 图 3.47:电压转换为浮动失败
10. 现在,将以下铸造操作员添加到`电压`等级:
```cpp
operator float() const
{
return m_emf;
}
```
11. Click on the **Run** button to recompile the code and run it:

###### 图 3.48:成功将电压转换为浮动
12. Now, place the `explicit` keyword in front of the cast that we just added and click on the **Run** button to recompile the code. Again, we get an error:

###### 图 3.49:无法将电压转换为浮动
13. 通过将显式声明添加到强制转换中,我们可以防止编译器使用转换运算符。更改带有错误的行,将伏特变量转换为浮点数:
```cpp
use_float(static_cast(volts));
```
14. 点击**运行**按钮重新编译代码并运行。

###### 图 3.50:电压转换成浮子,再次铸造
在本练习中,我们已经看到隐式转换可以发生在类型之间(而不是引用之间),并且我们可以控制它们何时发生。现在我们知道如何控制这些转换,我们可以努力满足之前引用的指南`C.46`和`C.164`。
### 活动 2:实现日期计算类离子
您的团队负责开发一个库来帮助进行与日期相关的计算。特别是,我们希望能够确定两个日期和给定日期之间的天数,增加(或减去)天数以获得新的日期。本活动将开发两种新类型,并对它们进行增强,以确保程序员不会意外地让它们与内置类型进行交互。按照以下步骤实现:
1. 设计并实现一个`日期`类,将`日`、`月`和`年`存储为整数。
2. 添加访问内部日、月和年值的方法。
3. 定义一个类型,`date_t`来表示自 1970 年 1 月 1 日`纪元日期`以来的天数。
4. 向`Date`类添加一个方法,将其转换为`date_t`。
5. 从`日期 _t`值中添加一个设置`日期`类的方法。
6. 创建一个存储天数值的`Days`类。
7. 在以`日`为自变量的`日`上加上`加法`运算符。
8. 使用`显式`防止数字相加。
9. 加上`减法`运算符,从两个`日期`的`差值`中返回一个`天数`值。
完成这些步骤后,您应该会收到以下输出:

###### 图 3.51:成功的日期示例应用的输出
#### 注意
这项活动的解决方案可以在第 664 页找到。
## 苏〔t0〕麦理
在这一章中,我们探讨了变量的生命周期——自动的和动态的,它们存储在哪里,以及何时被析构。然后,我们使用这些信息开发了 **RAII** 技术,该技术允许我们几乎忽略资源管理,因为即使在出现异常的情况下,自动变量也会在它们被析构时清理它们。然后,我们研究了抛出异常并捕捉它们,以便我们可以在正确的级别处理异常情况。从 **RAII** 开始,我们开始讨论资源的所有权以及 **STL** 智能指针如何在这方面帮助我们。我们发现几乎所有的事情都被视为函数调用,因此允许运算符重载和隐式转换。我们发现了奇妙的(或者是可怕的?)依赖于参数的查找世界 ( **ADL** )以及它如何可能在未来绊倒我们。我们现在对 C++ 的基本特性有了很好的理解。在下一章中,我们将开始探索函数对象,以及如何使用 lambda 函数实现它们。当我们再次访问封装时,我们将深入研究 STL 的产品并探索 PIMPLs。
================================================
FILE: docs/adv-cpp/05.md
================================================
# 五、关注点分离——软件架构、函数和可变模板
## 学习目标
本章结束时,您将能够:
* 使用 PIMPL 习惯用法开发类来实现对象级封装
* 使用函子、标准::函数和 lambda 表达式实现回调系统
* 根据情况使用正确的捕获技术实现 lambdas
* 开发变量模板来实现 C#风格的事件处理委托。
本章将向您展示如何实现 PIMPL 习惯用法,以及如何为您自己的程序开发回调机制。
## 简介
在前一章中,我们学习了如何使用 RAII 实现类来正确管理资源,即使异常发生时也是如此。我们还学习了 ADL ( **自变量相关查找**)以及它如何确定要调用的函数。最后,我们讨论了如何使用显式关键字来防止编译器在类型之间进行自动转换,这就是所谓的隐式转换
在本章中,我们将研究物理和逻辑的依赖关系,看看它们如何对构建时间产生负面影响。我们还将学习如何将可见接口类从实现细节中分离出来,以提高构建速度。然后,我们将学习捕获函数和上下文,以便稍后使用`函子`、`std::function`和`lambda 表达式`调用它们。最后,我们将实现一个变量模板来提供一个基于事件的回调机制。
### 实现的指针(PIMPL)成语
随着用 C++ 实现的项目越来越大,构建时间可能会以比文件数量更快的速度增长。这是因为 C++ 构建模型使用了文本包含模型。这样做是为了让编译器能够确定类的大小和布局,导致`调用方`和`被调用方`之间的耦合,但允许优化。请记住,在使用之前,必须定义所有内容。一个名为`模块`的未来特性有望解决这个问题,但是现在我们需要了解这个问题以及用于解决这个问题的技术。
### 逻辑和物理依赖关系
当我们希望从另一个类访问一个类时,我们有一个逻辑依赖。一个类在逻辑上依赖于另一个类。如果考虑到我们在*章 2A**中开发的`图形`类、`点 3d`和`矩阵 3d`,不允许鸭子-类型和演绎*和*第 3 章*、*可以和应该之间的距离-对象、指针和继承*,我们有两个逻辑上独立的类`矩阵 3d`和`点 3d`。然而,由于我们如何实现两者之间的乘法运算符,我们创建了编译时或**物理依赖关系**。

###### 图 4.1:矩阵 3d 和点 3d 的物理依赖关系
我们可以看到,对于这些相对简单的类,头文件和实现文件之间的物理依赖关系可能会很快变得复杂。正是这种复杂性导致了大型项目的构建时间,因为物理(和逻辑)依赖项的数量增长到了数千个。在前面的图中,我们只显示了 13 个依赖项,如箭头所示。但实际上还有很多,因为包含标准库头通常会引入包含文件的层次结构。这意味着,如果一个头文件被修改,那么所有依赖于它的文件,无论是直接的还是间接的,都需要被重新编译以解释这个变化。如果对私有类成员定义的更改是该类的用户甚至不能访问的,也会发生这种重建触发器。
为了帮助加快编译时间,我们使用了保护技术来防止头文件被多次处理:
```cpp
#if !defined(MY_HEADER_INCLUDED)
#define MY_HEADER_INCLUDED
// definitions
#endif // !defined(MY_HEADER_INCLUDED)
```
最近,大多数编译器都支持`#pragma 一次`指令,这也达到了同样的效果。
实体(文件、类等)之间的这些关系称为**耦合**。如果对文件/类的更改导致对另一个文件/类的更改,则该文件/类是与另一个文件/类高度耦合的**。如果对文件/类的更改不会导致对其他文件/类的更改,则文件/类是**松散耦合到另一个文件/类的**。**
高度耦合的代码(文件/类)会给项目带来问题。高度耦合的代码很难改变(不灵活),很难测试,也很难理解。另一方面,松散耦合的代码更容易更改(只修改一个类),可测试性更高(只需要被测试的类),更容易阅读和理解。耦合反映了逻辑和物理的依赖关系,并与之相关。
### 实现的指针(PIMPL)成语
耦合问题的一个解决方案是使用“**pumpol 习语**”(代表**指向实现习语**)。这也被称为不透明指针、编译器防火墙习惯用法甚至是**柴郡猫技术**。考虑 **Qt 库**,特别是 **Qt 平台抽象** ( **QPA** )。它是一个抽象层,隐藏了 Qt 应用所在的操作系统和/或平台的细节。实现这种层的一种方法是使用 PIMPL 习惯用法,其中公共接口向应用开发人员公开,但是如何交付功能的实现是隐藏的。Qt 实际上使用了 PIMPL 的一种变体,称为 d 指针。
例如,图形用户界面的一个特点是使用对话框,这是一个显示信息或提示用户输入的弹出窗口。可以在**对话框中声明如下:**
#### 注意
有关 QT 平台抽象(QPA)的更多信息,请访问以下链接:[https://doc.qt.io/qt-5/qpa.html#](https://doc.qt.io/qt-5/qpa.html#)。
```cpp
#pragma once
class Dialog
{
public:
Dialog();
~Dialog();
void create(const char* message);
bool show();
private:
struct DialogImpl;
DialogImpl* m_pImpl;
};
```
用户可以访问使用`对话框`所需的所有功能,但不知道如何实现。注意,我们有一个声明的`对话模板`,但没有定义它。总的来说,像`对话模板`这样的类我们无能为力。但是有一件事是允许的,那就是声明一个指向它的指针。C++ 的这个特性允许我们在实现文件中隐藏实现细节。这意味着在这个简单的例子中,我们没有这个声明的任何包含文件。
实现文件 **dialogImpl.cpp** 可以实现为:
```cpp
#include "dialog.hpp"
#include
#include
struct Dialog::DialogImpl
{
void create(const char* message)
{
m_message = message;
std::cout << "Creating the Dialog\n";
}
bool show()
{
std::cout << "Showing the message: '" << m_message << "'\n";
return true;
}
std::string m_message;
};
Dialog::Dialog() : m_pImpl(new DialogImpl)
{
}
Dialog::~Dialog()
{
delete m_pImpl;
}
void Dialog::create(const char* message)
{
m_pImpl->create(message);
}
bool Dialog::show()
{
return m_pImpl->show();
}
```
我们注意到以下几点:
* 在定义 Dialog 所需的方法之前,我们先定义实现类`DialogImpl`。这是必要的,因为`对话框`将需要通过`m _ Pippl`来练习这些方法,这意味着需要首先定义它们。
* `对话框`构造器和析构器负责内存管理。
* 我们只在实现文件中包含实现所需的所有必要头文件。这通过最小化包含在 **Dialog.hpp** 文件中的头的数量来减少耦合。
该程序可以按如下方式执行:
```cpp
#include
#include "dialog.hpp"
int main()
{
std::cout << "\n\n------ Pimpl ------\n";
Dialog dialog;
dialog.create("Hello World");
if (dialog.show())
{
std::cout << "Dialog displayed\n";
}
else
{
std::cout << "Dialog not displayed\n";
}
std::cout << "Complete.\n";
return 0;
}
```
执行时,上述程序产生以下输出:

###### 图 4.2:示例皮条客实现输出
### PIMPL 的优势和劣势
使用 PIMPL 的最大优势是它打破了类的客户端和它的实现之间的编译时依赖关系。这允许更快的构建时间,因为 PIMPL 消除了定义(头)文件中大量的`#include`指令,而是将它们推到只在实现文件中是必需的。
它还将实现与客户端分离。我们现在可以自由地更改 PIMPL 类的实现,只有那个文件需要重新编译。这可防止对隐藏成员的更改触发客户端重建的编译级联。这被称为编译防火墙。
PIMPL 成语的其他一些优点如下:
* **数据隐藏**–实现的内部细节真正隔离在实现类中。如果这是库的一部分,那么它可以用来防止信息的泄露,例如知识产权。
* **二进制兼容性**–类的二进制接口现在独立于私有字段。这意味着我们可以在不影响客户端的情况下向实现中添加字段。这也意味着我们可以在共享库中部署实现类(`DLL`,或者`)。所以`文件),并且可以在不影响客户端代码的情况下自由更改。
这样的优势是有代价的。缺点如下:
* **维护工作**–在可见类中有额外的代码将调用转发给实现类。这增加了间接性,但复杂性略有增加。
* **内存管理**–为实现增加一个指针,现在需要我们管理内存。它还需要额外的存储来保存指针,在内存受限的系统(例如:物联网设备)中,这可能是至关重要的。**T3】**
### 用独特的 _ptr 实现 PIMPL<>
我们当前的 Dialog 实现使用一个原始指针来保存 PIMPL 实现引用。在*第 3 章*、*能与应的距离——对象、指针和继承*中,我们讨论了对象的所有权,并引入了智能指针和 RAII。PIMPL 指针指向的隐藏对象是要管理的资源,应该使用`RAII`和`std::unique_ptr`来执行。正如我们将看到的,对于使用`std::unique_ptr`实现`PIMPL`有一些警告。
让我们将对话框实现改为使用智能指针。首先,头文件改变引入`#包含<内存>`行,析构函数可以移除,因为`unique_ptr`会自动删除实现类。
```cpp
#pragma once
#include
class Dialog
{
public:
Dialog();
void create(const char* message);
bool show();
private:
struct DialogImpl;
std::unique_ptr m_pImpl;
};
```
显然,我们从实现文件中移除了析构函数,并且修改了构造函数以使用`std::make_unique`。
```cpp
Dialog::Dialog() : m_pImpl(std::make_unique())
{
}
```
重新编译我们的新版本时, **Dialog.hpp** 和 **DialogImpl.cpp** 文件没有问题,但是我们的客户端 **main.cpp** 报告了以下错误(gcc 编译器),如下所示:

###### 图 4.3:使用 unique_ptr 编译皮条客失败
第一个错误报告**将“sizeof”无效应用于不完整的类型“Dialog::Dialog impl”**。问题是在 **main.cpp** 文件中,当`main()`函数结束时,编译器试图为我们调用`对话框`的析构函数。正如我们在*章节**【2A】**不允许鸭子–类型和演绎*中所讨论的,编译器会为我们生成一个析构函数(当我们移除它时)。这个生成的析构函数将调用`unique_ptr`的析构函数,这就是错误的原因。如果我们看一下 **unique_ptr.h** 文件的`第 76 行`,我们会发现`运算符()`函数对于`unique_ptr`使用的默认`deleter`的实现如下(该`deleter`是`unique_ptr`在破坏它所指向的对象时调用的函数):
```cpp
void
operator()(_Tp* __ptr) const
{
static_assert(!is_void<_Tp>::value, "can't delete pointer to incomplete type");
static_assert(sizeof(_Tp)>0, "can't delete pointer to incomplete type");
delete __ptr;
}
```
我们的代码在第二个`static_assert()`语句上失败,该语句终止编译并出现错误。问题是编译器试图为`STD::unique _ ptr`生成析构函数,而`DialogImpl`是不完整的类型。因此,为了解决这个问题,我们将析构函数的生成控制在`DialogImpl`是一个完整类型的点上。
为此,我们将析构函数的声明放回类中,并将其实现添加到`DialogImpl.cpp`文件中。
```cpp
Dialog::~Dialog()
{
}
```
当我们编译和运行我们的程序时,它会产生与以前完全相同的输出。事实上,如果我们只需要一个空析构函数,我们可以用下面的代码替换上面的代码:
```cpp
Dialog::~Dialog() = default;
```
如果我们编译并运行我们的程序,那么将产生以下输出:

###### 图 4.4:示例 unique_ptr Pimpl 实现输出
### 独特 _ptr < > PIMPL 特殊功能
正如 PIMPL 通常暗示的那样,可见接口类拥有实现类,移动语义是自然的。但是,同样编译器生成的析构函数实现是正确的,编译器生成的移动构造函数和移动赋值运算符会给出想要的行为,即对成员`unique_ptr`执行移动。移动操作都可能需要在赋值之前执行删除操作,因此会遇到与类型不完整的析构函数相同的问题。解决方案与析构函数相同——在头文件中声明方法,当类型完成时在实现文件中实现。因此,我们的头文件如下所示:
```cpp
class Dialog
{
public:
Dialog();
~Dialog();
Dialog(Dialog&& rhs);
Dialog& operator=(Dialog&& rhs);
void create(const char* message);
bool show();
private:
struct DialogImpl;
std::unique_ptr m_pImpl;
};
```
虽然实现看起来像:
```cpp
Dialog::Dialog() : m_pImpl(std::make_unique())
{
}
Dialog::~Dialog() = default;
Dialog::Dialog(Dialog&& rhs) = default;
Dialog& Dialog::operator=(Dialog&& rhs) = default;
```
根据我们隐藏在实现类中的数据项,我们可能还需要 PIMPL 类的复制功能。在对话框类中使用`std::unique_ptr`可以防止自动生成复制构造函数和复制赋值运算符,因为内部成员不支持复制。此外,通过定义移动成员函数,正如我们在*章节【2A】*、*不允许鸭子–类型和演绎*中看到的,它也停止编译器生成副本版本。另外,如果编译器确实为我们生成了拷贝语义,那也只是**浅拷贝**。但是由于 PIMPL 的实现,我们需要一个**深度副本**。所以,我们需要编写自己的复制特殊成员函数。同样,定义在头文件中,实现需要在类型完整的地方完成,在 **DialogImpl.cpp** 文件中。
在头文件中,我们添加了以下声明:
```cpp
Dialog(const Dialog& rhs);
Dialog& operator=(const Dialog& rhs);
```
实现如下所示:
```cpp
Dialog::Dialog(const Dialog& rhs) : m_pImpl(nullptr)
{
if (this == &rhs) // do nothing on copying self
return;
if (rhs.m_pImpl) // rhs has something -> clone it
m_pImpl = std::make_unique(*rhs.m_pImpl);
}
Dialog& Dialog::operator=(const Dialog& rhs)
{
if (this == &rhs) // do nothing on assigning to self
return *this;
if (!rhs.m_pImpl) // rhs is empty -> delete ours
{
m_pImpl.reset();
}
else if (!m_pImpl) // ours is empty -> clone rhs
{
m_pImpl = std::make_unique(*rhs.m_pImpl);
}
else // use copy of DialogImpl
{
*m_pImpl = *rhs.m_pImpl;
}
}
```
注意`if(this == & rhs)`子句。这些是为了防止对象不必要地复制自己。另外,请注意,我们需要检查`unique_ptr`是否为空,并相应地处理副本。
#### 注意
在解决本章的任何实际问题之前,下载 GitHub 资源库[https://github.com/TrainingByPackt/Advanced-CPlusPlus](https://github.com/TrainingByPackt/Advanced-CPlusPlus)并导入 Eclipse 中第 4 课的文件夹,这样您就可以查看每个练习和活动的代码。
### 练习 1:用独特的 T2 实现厨房
在本练习中,我们将通过使用`unique_ptr < >`实现`皮条客成语`来隐藏厨房如何处理订单的细节。按照以下步骤实施本练习:
1. 在 Eclipse 中打开**第 4 课**项目,然后在**项目浏览器**中,依次展开**第 4 课**和**练习 01** ,双击**练习 1.cpp** 将本练习的文件打开到编辑器中。
2. 由于这是一个基于 CMake 的项目,将当前的构建器改为 CMake Build(可移植)。
3. 点击**启动配置**下拉菜单,选择**新启动配置……**。配置**l4 练习 1** 以名称**练习 1** 运行。
4. Click on the **Run** button. Exercise 1 will run and produce the following output:

###### 图 4.5:练习 1 程序输出
5. Open **kitchen.hpp** in the editor, and you will find the following declaration:
```cpp
class Kitchen
{
public:
Kitchen(std::string chef);
std::string processOrder(std::string order);
private:
std::string searchForRecipe(std::string order);
std::string searchForDessert(std::string order);
std::string cookRecipe(std::string recipe);
std::string serveDessert(std::string dessert);
std::vector::iterator getRecipe(std::string recipe);
std::vector::iterator getDessert(std::string recipe);
std::string m_chef;
std::vector m_recipes;
std::vector m_desserts;
};
```
私人部分的所有内容都是关于厨房如何向顾客交付订单的细节。它强制包含头文件**食谱. hpp** 和**甜点. hpp** ,在这些细节文件和`厨房`的客户之间建立了一个耦合。我们将把所有私有成员移到一个实现类中,并隐藏细节。
6. 在 **kitchen.hpp** 文件中,添加`#include < memory >`指令以访问`unique_ptr`。添加析构函数`~Kitchen()的声明;`然后在私段顶部增加以下两行:
```cpp
struct Impl;
std::unique_ptr m_impl;
```
7. 打开**厨房. cpp** 文件,在`#包括`指令后添加以下内容:
```cpp
struct Kitchen::Impl
{
};
Kitchen::~Kitchen() = default;
```
8. 点击**运行**按钮重新构建程序。您会看到输出仍然和以前一样。
9. 从 **kitchen.hpp** 中的`Kitchen`类中移除除两个新成员之外的所有私有成员,并将其添加到`Kitchen::Impl`声明中。**厨房. hpp** 文件删除`#包含<矢量>`、`#包含【食谱. HPP】`、`#包含【甜点. HPP】`:
```cpp
#pragma once
#include
#include
class Kitchen
{
public:
Kitchen(std::string chef);
~Kitchen();
std::string processOrder(std::string order);
private:
struct Impl;
std::unique_ptr m_impl;
};
```
后内容如下
10. 在 **kitchen.cpp** 文件中,将 kitchen 构造函数更改为`Kitchen::Impl`构造函数:
```cpp
Kitchen::Impl::Impl(std::string chef) : m_chef{chef}
```
11. 对于原始方法的其余部分,将其范围更改为`厨房::Impl`而不是`厨房::`。例如,`STD::string Kitchen::process order(STD::string order)`变为`STD::string Kitchen::Impl::process order(STD::string order)`。
12. 在`Kitchen::Impl`中,添加一个带有`std::string`参数和`processOrder()`方法的构造函数。`厨房::Impl`声明现在应该如下所示:
```cpp
struct Kitchen::Impl
{
Impl(std::string chef);
std::string processOrder(std::string order);
std::string searchForRecipe(std::string order);
std::string searchForDessert(std::string order);
std::string cookRecipe(std::string recipe);
std::string serveDessert(std::string dessert);
std::vector::iterator getRecipe(std::string recipe);
std::vector::iterator getDessert(std::string recipe);
std::string m_chef;
std::vector m_recipes;
std::vector m_desserts;
};
```
13. 在 **kitchen.cpp** 中,在文件顶部添加`#include < vector >`、`# include“recipe . HPP”`、`#include“甜品. HPP”`。
14. 点击**运行**按钮重新构建程序,这次会出现两个未定义的引用失败–`厨房:【厨房】`和`厨房:【过程顺序】`。
15. 在 **Kitchen.cpp** 中,在`Kitchen::Impl`方法定义后,添加以下两个方法:
```cpp
Kitchen::Kitchen(std::string chef) : m_impl(std::make_unique(chef))
{
}
std::string Kitchen::processOrder(std::string order)
{
return m_impl->processOrder(order);
}
```
16. 点击**运行**按钮重新构建程序。程序将再次运行以产生原始输出。

###### 图 4.6:使用皮条客的厨房程序输出
在本练习中,我们采用了一个在其私有成员中包含许多细节的类,并将这些细节移动到一个 PIMPL 类中,以隐藏细节并使用前面描述的技术将接口与实现分离。
## 函数对象和λ表达式
编程中使用的一种常见模式,尤其是在实现基于事件的处理(如异步输入和输出)时,是使用**回调**。客户端注册希望收到事件发生的通知(例如:数据可供读取,或者数据传输已完成)。这种模式被称为**观察者模式**或**订户发布者模式**。C++ 支持多种技术来提供回调机制。
### 函数指针
第一种机制是使用**函数指针**。这是继承自 C 语言的遗留特性。下面的程序显示了一个函数指针的例子:
```cpp
#include
using FnPtr = void (*)(void);
void function1()
{
std::cout << "function1 called\n";
}
int main()
{
std::cout << "\n\n------ Function Pointers ------\n";
FnPtr fn{function1};
fn();
std::cout << "Complete.\n";
return 0;
}
```
该程序在编译和执行时会产生以下输出:

###### 图 4.7:函数指针程序输出
严格来说,代码应该修改如下:
```cpp
FnPtr fn{&function1};
if(fn != nullptr)
fn();
```
首先要注意的是,(`&`)运算符的地址应该用来初始化指针。其次,我们应该在调用指针之前检查它是否有效。
```cpp
#include
using FnPtr = void (*)(void);
struct foo
{
void bar() { std::cout << "foo:bar called\n"; }
};
int main()
{
std::cout << "\n\n------ Function Pointers ------\n";
foo object;
FnPtr fn{&object.bar};
fn();
std::cout << "Complete.\n";
return 0;
}
```
当我们试图编译这个程序时,我们会得到以下错误:

###### 图 4.8:编译函数指针程序时的错误
第一个错误的文本是 **ISO C++ 禁止取绑定成员函数的地址形成成员函数的指针。说'& foo::bar'** 。它告诉我们应该使用不同的形式来获取函数地址。这种情况下真正的错误是第二条错误消息:**错误:初始化**时无法将“void (foo::*)()”转换为“FnPtr { aka void(*)}”。真正的问题是,非静态成员函数在被调用时会有一个隐藏的参数——这个**指针。**
通过将上述程序更改为以下内容:
```cpp
#include
using FnPtr = void (*)(void);
struct foo
{
static void bar() { std::cout << "foo:bar called\n"; }
};
int main()
{
std::cout << "\n\n------ Function Pointers ------\n";
FnPtr fn{&foo::bar};
fn();
std::cout << "Complete.\n";
return 0;
}
```
它成功编译并运行:

###### 图 4.9:使用静态成员函数的函数指针程序
当与使用回调和支持回调的操作系统通知的 C 库接口时,经常使用函数指针技术。在这两种情况下,回调接受一个参数是用户注册的数据 blob 指针的`void *`是正常的。数据块指针可以是类的`这个`指针,然后被取消引用,回调被转发到成员函数中。
在其他语言中,例如 Python 和 C#,捕捉函数指针也将捕捉调用该函数所需的足够数据,这是语言的一部分(例如:`self`或`this`)。C++ 能够通过函数调用操作符调用任何对象,我们接下来将介绍这一点。
### 什么是功能对象?
C++ 允许函数调用运算符`运算符()`重载。这就产生了使任何物体`成为可调用的`的能力。一个可调用的对象被称为**函子**。下面程序中的`Scaler`类实现了一个`函子`。
```cpp
struct Scaler
{
Scaler(int scale) : m_scale{scale} {};
int operator()(int value)
{
return m_scale * value;
}
int m_scale{1};
};
int main()
{
std::cout << "\n\n------ Functors ------\n";
Scaler timesTwo{2};
Scaler timesFour{4};
std::cout << "3 scaled by 2 = " << timesTwo(3) << "\n";
std::cout << "3 scaled by 4 = " << timesFour(3) << "\n";
std::cout << "Complete.\n";
return 0;
}
```
创建了两个类型为`缩放器`的对象,它们被用作生成输出的线内的函数。上述程序产生以下输出:

###### 图 4.10:函子程序输出
`函子`相对于函数指针的一个优势是,它们可以包含状态,作为一个对象或者跨所有实例。另一个优点是,它们可以传递给期望函数(例如`标准::for_each`)或运算符(例如`标准::transform`)的 STL 算法。
这种用途的示例可能如下所示:
```cpp
#include
#include
#include
struct Scaler
{
Scaler(int scale) : m_scale{scale} {};
int operator()(int value)
{
return m_scale * value;
}
int m_scale{1};
};
void PrintVector(const char* prefix, std::vector& values)
{
const char* sep = "";
std::cout << prefix << " = [";
for(auto n : values)
{
std::cout << sep << n;
sep = ", ";
}
std::cout << "]\n";
}
int main()
{
std::cout << "\n\n------ Functors with STL ------\n";
std::vector