Repository: apachecn/apachecn-c-cpp-zh Branch: master Commit: ef2fc7d4e461 Files: 587 Total size: 11.2 MB Directory structure: gitextract_a4dv877j/ ├── .gitignore ├── .nojekyll ├── 404.html ├── CNAME ├── Dockerfile ├── LICENSE ├── NAV.md ├── README.md ├── SUMMARY.md ├── asset/ │ ├── back-to-top.css │ ├── back-to-top.js │ ├── dark-mode.css │ ├── dark-mode.js │ ├── docsify-apachecn-footer.js │ ├── docsify-baidu-push.js │ ├── docsify-baidu-stat.js │ ├── docsify-clicker.js │ ├── docsify-cnzz.js │ ├── docsify-katex.js │ ├── docsify-quick-page.css │ ├── docsify-quick-page.js │ ├── edit.css │ ├── edit.js │ ├── prism-darcula.css │ ├── share.css │ ├── share.js │ ├── style.css │ └── vue.css ├── docs/ │ ├── .gitkeep │ ├── adv-cpp/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── adv-cpp-prog-cb/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── 11.md │ │ ├── 12.md │ │ ├── 13.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── app-dev-qt-creator/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── 11.md │ │ ├── 12.md │ │ ├── 13.md │ │ ├── 14.md │ │ ├── README.md │ │ ├── SUMMARY.md │ │ ├── sec1.md │ │ ├── sec2.md │ │ └── sec3.md │ ├── begin-cpp-game-prog/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── 11.md │ │ ├── 12.md │ │ ├── 13.md │ │ ├── 14.md │ │ ├── 15.md │ │ ├── 16.md │ │ ├── 17.md │ │ ├── 18.md │ │ ├── 19.md │ │ ├── 20.md │ │ ├── 21.md │ │ ├── 22.md │ │ ├── 23.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── begin-cpp-prog/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── boost-asio-cpp-net-prog-2e/ │ │ ├── 0.md │ │ ├── 1.md │ │ ├── 2.md │ │ ├── 3.md │ │ ├── 4.md │ │ ├── 5.md │ │ ├── 6.md │ │ ├── 7.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── boost-cpp-app-dev-cb/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── 11.md │ │ ├── 12.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── cpp-dsal-design-principle/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── cpp-gui-prog-qt5/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── 11.md │ │ ├── 12.md │ │ ├── 13.md │ │ ├── 14.md │ │ ├── 15.md │ │ ├── 16.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── cpp-hiperf/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── 11.md │ │ ├── 12.md │ │ ├── 13.md │ │ ├── 14.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── cpp-react-prog/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── 11.md │ │ ├── 12.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── cpp-sys-prog-cb/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── 11.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── cpp-workshop/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── 11.md │ │ ├── 12.md │ │ ├── 13.md │ │ ├── 14.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── emb-prog-mod-cpp-cb/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── 11.md │ │ ├── 12.md │ │ ├── 13.md │ │ ├── 14.md │ │ ├── 15.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── exp-cpp/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── 11.md │ │ ├── 12.md │ │ ├── 13.md │ │ ├── 14.md │ │ ├── 15.md │ │ ├── 16.md │ │ ├── 17.md │ │ ├── README.md │ │ ├── SUMMARY.md │ │ ├── sec1.md │ │ ├── sec2.md │ │ └── sec3.md │ ├── exp-cpp-prog/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── game-dev-proj-ue/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── 11.md │ │ ├── 12.md │ │ ├── 13.md │ │ ├── 14.md │ │ ├── 15.md │ │ ├── 16.md │ │ ├── 17.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── handson-cpp-game-ani-prog/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── 11.md │ │ ├── 12.md │ │ ├── 13.md │ │ ├── 14.md │ │ ├── 15.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── handson-func-prog-cpp/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── 11.md │ │ ├── 12.md │ │ ├── 13.md │ │ ├── 14.md │ │ ├── 15.md │ │ ├── 16.md │ │ ├── 17.md │ │ ├── README.md │ │ ├── SUMMARY.md │ │ ├── sec1.md │ │ ├── sec2.md │ │ ├── sec3.md │ │ └── sec4.md │ ├── handson-game-dev-wasm/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── 11.md │ │ ├── 12.md │ │ ├── 13.md │ │ ├── 14.md │ │ ├── 15.md │ │ ├── 16.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── lcthw-zh/ │ │ ├── README.md │ │ ├── SUMMARY.md │ │ ├── donors.md │ │ ├── ex0.md │ │ ├── ex1.md │ │ ├── ex10.md │ │ ├── ex11.md │ │ ├── ex12.md │ │ ├── ex13.md │ │ ├── ex14.md │ │ ├── ex15.md │ │ ├── ex16.md │ │ ├── ex17.md │ │ ├── ex18.md │ │ ├── ex19.md │ │ ├── ex2.md │ │ ├── ex20.md │ │ ├── ex21.md │ │ ├── ex22.md │ │ ├── ex23.md │ │ ├── ex24.md │ │ ├── ex25.md │ │ ├── ex26.md │ │ ├── ex27.md │ │ ├── ex28.md │ │ ├── ex29.md │ │ ├── ex3.md │ │ ├── ex30.md │ │ ├── ex31.md │ │ ├── ex32.md │ │ ├── ex33.md │ │ ├── ex34.md │ │ ├── ex35.md │ │ ├── ex36.md │ │ ├── ex37.md │ │ ├── ex38.md │ │ ├── ex39.md │ │ ├── ex4.md │ │ ├── ex40.md │ │ ├── ex41.md │ │ ├── ex42.md │ │ ├── ex43.md │ │ ├── ex44.md │ │ ├── ex45.md │ │ ├── ex46.md │ │ ├── ex47.md │ │ ├── ex5.md │ │ ├── ex6.md │ │ ├── ex7.md │ │ ├── ex8.md │ │ ├── ex9.md │ │ ├── introduction.md │ │ ├── postscript.md │ │ └── preface.md │ ├── learn-cpp-build-game-ue4/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── 11.md │ │ ├── 12.md │ │ ├── 13.md │ │ ├── 14.md │ │ ├── 15.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── learn-cpp-func-prog/ │ │ ├── 0.md │ │ ├── 1.md │ │ ├── 2.md │ │ ├── 3.md │ │ ├── 4.md │ │ ├── 5.md │ │ ├── 6.md │ │ ├── 7.md │ │ ├── 8.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── learn-cuda-prog/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── 11.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── learn-qt5/ │ │ ├── 0.md │ │ ├── 1.md │ │ ├── 2.md │ │ ├── 3.md │ │ ├── 4.md │ │ ├── 5.md │ │ ├── 6.md │ │ ├── 7.md │ │ ├── 8.md │ │ ├── 9.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── learn-wasm/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── master-cpp-game-dev/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── 11.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── master-cpp-multithrd/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── master-cpp-prog/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── mod-cpp/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── 11.md │ │ ├── 12.md │ │ ├── 13.md │ │ ├── 14.md │ │ ├── 15.md │ │ ├── 16.md │ │ ├── 17.md │ │ ├── README.md │ │ └── SUMMARY.md │ ├── mod-cpp-challenge/ │ │ ├── 00.md │ │ ├── 01.md │ │ ├── 02.md │ │ ├── 03.md │ │ ├── 04.md │ │ ├── 05.md │ │ ├── 06.md │ │ ├── 07.md │ │ ├── 08.md │ │ ├── 09.md │ │ ├── 10.md │ │ ├── 11.md │ │ ├── 12.md │ │ ├── 13.md │ │ ├── README.md │ │ └── SUMMARY.md │ └── qt5-cpp-gui-prog-cb/ │ ├── 00.md │ ├── 01.md │ ├── 02.md │ ├── 03.md │ ├── 04.md │ ├── 05.md │ ├── 06.md │ ├── 07.md │ ├── 08.md │ ├── 09.md │ ├── 10.md │ ├── 11.md │ ├── 12.md │ ├── 13.md │ ├── README.md │ └── SUMMARY.md ├── index.html └── update.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # dotenv .env # virtualenv .venv venv/ ENV/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .DS_Store # gitbook _book # node.js node_modules # windows Thumbs.db # word ~$*.docx ~$*.doc ================================================ FILE: .nojekyll ================================================ ================================================ FILE: 404.html ================================================ --- permalink: /404.html --- ================================================ FILE: CNAME ================================================ ccpp.apachecn.org ================================================ FILE: Dockerfile ================================================ FROM httpd:2.4 COPY ./ /usr/local/apache2/htdocs/ ================================================ FILE: LICENSE ================================================ Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License (CC BY-NC-SA 4.0) Copyright © 2020 ApacheCN(apachecn@163.com) By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. Section 1 – Definitions. a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. c. BY-NC-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License. d. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. e. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. f. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. g. License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution, NonCommercial, and ShareAlike. h. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. i. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. j. Licensor means the individual(s) or entity(ies) granting rights under this Public License. k. NonCommercial means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange. l. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. m. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. n. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. Section 2 – Scope. a. License grant. 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: A. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and B. produce, reproduce, and Share Adapted Material for NonCommercial purposes only. 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 3. Term. The term of this Public License is specified in Section 6(a). 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. 5. Downstream recipients. A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. B. Additional offer from the Licensor – Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply. C. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). b. Other rights. 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 2. Patent and trademark rights are not licensed under this Public License. 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes. Section 3 – License Conditions. Your exercise of the Licensed Rights is expressly made subject to the following conditions. a. Attribution. 1. If You Share the Licensed Material (including in modified form), You must: A. retain the following if it is supplied by the Licensor with the Licensed Material: i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); ii. a copyright notice; iii. a notice that refers to this Public License; iv. a notice that refers to the disclaimer of warranties; v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. b. ShareAlike. In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. 1. The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-NC-SA Compatible License. 2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material. 3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. Section 4 – Sui Generis Database Rights. Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only; b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. Section 5 – Disclaimer of Warranties and Limitation of Liability. a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. Section 6 – Term and Termination. a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 2. upon express reinstatement by the Licensor. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. Section 7 – Other Terms and Conditions. a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. Section 8 – Interpretation. a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. ================================================ FILE: NAV.md ================================================ + 编程 + [JavaTPoint 编程语言中文教程📚](https://apachecn.github.io/javatpoint-prog-zh) + [JavaTPoint .NET 中文教程📚](https://apachecn.github.io/javatpoint-dotnet-zh) + [JavaTPoint Java 中文教程📚](https://apachecn.github.io/javatpoint-java-zh) + [JavaTPoint Python 中文教程📚](https://apachecn.github.io/javatpoint-python-zh) + [GeeksForGeeks 编程语言杂项中文教程📚](https://apachecn.github.io/geeksforgeeks-lang-misc-zh) + [GeeksForGeeks C# 中文教程📚](https://apachecn.github.io/geeksforgeeks-csharp-zh) + [GeeksForGeeks Scala 中文教程📚](https://apachecn.github.io/geeksforgeeks-scala-zh) + [GeeksForGeeks Python 中文教程📚](https://apachecn.github.io/geeksforgeeks-python-zh) + [GeeksForGeeks C/C++ 中文教程📚](https://apachecn.github.io/geeksforgeeks-c-cpp-zh) + [GeeksForGeeks Java 中文教程📚](https://apachecn.github.io/geeksforgeeks-java-zh) + [GeeksForGeeks JavaScript 中文教程📚](https://apachecn.github.io/geeksforgeeks-js-zh) + [ApacheCN C# 译文集📚](https://apachecn.github.io/apachecn-csharp-zh) + [ApacheCN C# 译文集(二)📚](https://apachecn.github.io/apachecn-csharp-zh-pt2) + [ApacheCN C# 译文集(三)📚](https://apachecn.github.io/apachecn-csharp-zh-pt3) + [ApacheCN C# 译文集(四)📚](https://apachecn.github.io/apachecn-csharp-zh-pt4) + [ApacheCN Golang 译文集📚](https://apachecn.github.io/apachecn-golang-zh) + [ApacheCN Golang 译文集(二)📚](https://apachecn.github.io/apachecn-golang-zh-pt2) + [ApacheCN C/C++ 译文集📚](https://apachecn.github.io/apachecn-c-cpp-zh) + [ApacheCN C/C++ 译文集(二)📚](https://apachecn.github.io/apachecn-c-cpp-zh-pt2) + [ApacheCN C/C++ 译文集(三)📚](https://apachecn.github.io/apachecn-c-cpp-zh-pt3) + [ApacheCN Java 译文集📚](https://apachecn.github.io/apachecn-java-zh) + [ApacheCN Java 译文集(二)📚](https://apachecn.github.io/apachecn-java-zh-pt2) + [ApacheCN Java 译文集(三)📚](https://apachecn.github.io/apachecn-java-zh-pt3) + [ApacheCN JavaScript 译文集📚](https://apachecn.github.io/apachecn-js-zh) + [ApacheCN JavaScript 译文集(二)📚](https://apachecn.github.io/apachecn-js-zh-pt2) + [ApacheCN JavaScript 译文集(三)📚](https://apachecn.github.io/apachecn-js-zh-pt3) + [ApacheCN JavaScript 译文集(四)📚](https://apachecn.github.io/apachecn-js-zh-pt4) + [ApacheCN Python 译文集📚](https://apachecn.github.io/apachecn-python-zh) + [ApacheCN Python 译文集(二)📚](https://apachecn.github.io/apachecn-python-zh-pt2) + [ApacheCN Python 译文集(三)📚](https://apachecn.github.io/apachecn-python-zh-pt3) + [ApacheCN Python 译文集(四)📚](https://apachecn.github.io/apachecn-python-zh-pt4) + [ApacheCN Ruby 译文集📚](https://apachecn.github.io/apachecn-ruby-zh) + [BeginnersBook 中文系列教程📚](https://apachecn.github.io/beginnersbook-zh) + [JavaScript 编程精解 中文第三版](https://apachecn.github.io/eloquent-js-3e-zh) + [Guru99 中文系列教程📚🚧](https://apachecn.github.io/guru99-zh) + [HowToDoInJava 中文系列教程📚](https://apachecn.github.io/howtodoinjava-zh) + [OverIQ 中文系列教程📚](https://apachecn.github.io/overiq-zh) + [LearnETutroials 中文系列教程📚](https://apachecn.github.io/learnetutorials-zh) + [StudyTonight 中文系列教程📚](https://apachecn.github.io/studytonight-zh) + [TutorialGateway 中文系列教程📚](https://apachecn.github.io/tutorialgateway-zh) + [TutorialGateway BI 中文系列教程📚](https://apachecn.github.io/tutorialgateway-bi-zh) + [TutorialsTeacher 中文系列教程📚](https://apachecn.github.io/tutorialsteacher-zh) + [通过示例学 Golang 2020 中文版](https://apachecn.github.io/golang-by-example-2020-zh) + [写给不耐烦程序员的 JavaScript🚧](https://apachecn.github.io/impatient-js-zh) + [JavaBeginnersTutorial 中文系列教程📚](https://apachecn.github.io/jbt-zh) + [JavaTutorialNetwork 中文系列教程📚](https://apachecn.github.io/jtn-zh) + [笨办法学C 中文版](https://apachecn.github.io/lcthw-zh) + [笨办法学 Python · 续 中文版](https://apachecn.github.io/lmpythw-zh) + [Programiz 中文系列教程📚](https://apachecn.github.io/programiz-zh) + [PythonBasics 中文系列教程📚](https://apachecn.github.io/pythonbasics-zh) + [PythonGuru 中文系列教程📚](https://apachecn.github.io/pythonguru-zh) + [PythonSpot 中文系列教程📚](https://apachecn.github.io/pythonspot-zh) + [Think Python](https://apachecn.github.io/think-py-2e-zh) + [ZetCode 中文系列教程📚](https://apachecn.github.io/zetcode-zh) + 前端 + [JavaTPoint 移动开发中文教程📚](https://apachecn.github.io/javatpoint-mobi-zh) + [GeeksForGeeks Web 杂项中文教程📚](https://apachecn.github.io/geeksforgeeks-web-misc-zh) + [GeeksForGeeks Angular/Vue/React 中文教程📚](https://apachecn.github.io/geeksforgeeks-ng-vue-react-zh) + [GeeksForGeeks jQuery 中文教程📚](https://apachecn.github.io/geeksforgeeks-jquery-zh) + [GeeksForGeeks CSS 中文教程📚](https://apachecn.github.io/geeksforgeeks-css-zh) + [GeeksForGeeks HTML 中文教程📚](https://apachecn.github.io/geeksforgeeks-html-zh) + [ApacheCN Vue 译文集📚](https://apachecn.github.io/apachecn-vue-zh) + [ApacheCN Angular 译文集📚](https://apachecn.github.io/apachecn-angular-zh) + [ApacheCN React 译文集📚](https://apachecn.github.io/apachecn-react-zh) + [ApacheCN jQuery 译文集📚](https://apachecn.github.io/apachecn-jquery-zh) + [ApacheCN jQuery 译文集(二)📚](https://apachecn.github.io/apachecn-jquery-zh-pt2) + 后端/大数据 + [JavaTPoint 大数据中文教程📚](https://apachecn.github.io/javatpoint-bigdata-zh) + [JavaTPoint Web 开发中文教程📚](https://apachecn.github.io/javatpoint-web-zh) + [JavaTPoint 数据库中文教程📚](https://apachecn.github.io/javatpoint-db-zh) + [JavaTPoint PHP 中文教程📚](https://apachecn.github.io/javatpoint-php-zh) + [GeeksForGeeks ASP 中文教程📚](https://apachecn.github.io/geeksforgeeks-asp-zh) + [GeeksForGeeks SQL 中文教程📚](https://apachecn.github.io/geeksforgeeks-sql-zh) + [GeeksForGeeks NodeJS 中文教程📚](https://apachecn.github.io/geeksforgeeks-nodejs-zh) + [GeeksForGeeks PHP 中文教程📚](https://apachecn.github.io/geeksforgeeks-php-zh) + [ApacheCN 数据库译文集📚](https://apachecn.github.io/apachecn-db-zh) + [ApacheCN 数据库译文集(二)📚](https://apachecn.github.io/apachecn-db-zh-pt2) + [ApacheCN Python Web 译文集📚](https://apachecn.github.io/apachecn-pythonweb-zh) + [ApacheCN Python Web 译文集(二)📚](https://apachecn.github.io/apachecn-pythonweb-zh-pt2) + [ApacheCN Asp.NET 译文集📚](https://apachecn.github.io/apachecn-asp-dotnet-zh) + [ApacheCN Asp.NET 译文集(二)📚](https://apachecn.github.io/apachecn-asp-dotnet-zh-pt2) + [ApacheCN Asp.NET 译文集(三)📚](https://apachecn.github.io/apachecn-asp-dotnet-zh-pt3) + [ApacheCN Asp.NET 译文集(四)📚](https://apachecn.github.io/apachecn-asp-dotnet-zh-pt4) + [ApacheCN NodeJS 译文集📚](https://apachecn.github.io/apachecn-node-zh) + [ApacheCN NodeJS 译文集(二)📚](https://apachecn.github.io/apachecn-node-zh-pt2) + [ApacheCN PHP 译文集📚](https://apachecn.github.io/apachecn-php-zh) + [ApacheCN PHP 译文集(二)📚](https://apachecn.github.io/apachecn-php-zh-pt2) + [ApacheCN 大数据译文集(二)📚](https://apachecn.github.io/apachecn-bigdata-zh-pt2) + [ApacheCN 大数据译文集(三)📚](https://apachecn.github.io/apachecn-bigdata-zh-pt3) + [ApacheCN 大数据译文集📚](https://apachecn.github.io/apachecn-bigdata-zh) + [ApacheCN Java Web 译文集📚](https://apachecn.github.io/apachecn-javaweb-zh) + [ApacheCN Java Web 译文集(二)📚](https://apachecn.github.io/apachecn-javaweb-zh-pt2) + [Airflow 中文文档](https://apachecn.github.io/airflow-doc-zh) + [Elasticsearch 5.4 中文文档](https://apachecn.github.io/elasticsearch-doc-zh) + [Flink 中文文档](https://apachecn.github.io/flink-doc-zh) + [HBase™ 中文参考指南 3.0🚧](https://apachecn.github.io/hbase-doc-zh) + [HighScalability 中文示例📚🚧](https://apachecn.github.io/highscalability-examples-zh) + [Kibana 5.2 中文文档](https://apachecn.github.io/kibana-doc-zh) + [Kudu 1.4.0 中文文档](https://apachecn.github.io/kudu-doc-zh) + [Apache Spark 官方文档中文版](https://apachecn.github.io/spark-doc-zh) + [Apache Kafka 官方文档中文版](https://apachecn.github.io/kafka-site-zh) + [Spring Boot 1.5.2 中文文档](https://apachecn.github.io/spring-boot-doc-zh) + [Storm 1.1.0 中文文档](https://apachecn.github.io/storm-doc-zh) + [Zeppelin 0.7.2 中文文档](https://apachecn.github.io/zeppelin-doc-zh) + 工具 + [JavaTPoint 实用工具中文教程📚](https://apachecn.github.io/javatpoint-util-zh) + [ApacheCN DevOps 译文集📚](https://apachecn.github.io/apachecn-devops-zh) + [ApacheCN DevOps 译文集(二)📚](https://apachecn.github.io/apachecn-devops-zh-pt2) + [ApacheCN DevOps 译文集(三)📚](https://apachecn.github.io/apachecn-devops-zh-pt3) + [ApacheCN DevOps 译文集(四)📚](https://apachecn.github.io/apachecn-devops-zh-pt4) + [ApacheCN DevOps 译文集(五)📚](https://apachecn.github.io/apachecn-devops-zh-pt5) + [ApacheCN Linux 译文集📚](https://apachecn.github.io/apachecn-linux-zh) + [ApacheCN Linux 译文集(二)📚](https://apachecn.github.io/apachecn-linux-zh-pt2) + [ApacheCN Linux 译文集(三)📚](https://apachecn.github.io/apachecn-linux-zh-pt3) + [Cython 3.0 中文文档🚧](https://apachecn.github.io/cython-doc-zh) + [Git 中文参考🚧](https://apachecn.github.io/git-doc-zh) + [Gitlab 中文文档🚧](https://apachecn.github.io/gitlab-doc-zh) + [笨办法学 Linux 中文版](https://apachecn.github.io/llthw-zh) + [Numba 0.44 中文文档🚧](https://apachecn.github.io/numba-doc-zh) + [PyQt4 中文文档🚧](https://apachecn.github.io/pyqt4-doc-zh) + [Scrapy 1.6 中文文档](https://apachecn.github.io/scrapy-doc-zh) + 数据科学 + [ApacheCN 数据科学译文集📚](https://apachecn.github.io/apachecn-ds-zh) + [ApacheCN 数据科学译文集(二)📚](https://apachecn.github.io/apachecn-ds-zh-pt2) + [ApacheCN 数据科学译文集(三)📚](https://apachecn.github.io/apachecn-ds-zh-pt3) + [ApacheCN 数据科学译文集📚](https://apachecn.github.io/apachecn-ds-zh) + [MIT 18.03 面向初学者的微积分🚧](https://apachecn.github.io/calc4b-zh) + [UCB Data8 计算与推断思维](https://apachecn.github.io/data8-textbook-zh) + [数据可视化的基础知识](https://apachecn.github.io/dataviz-zh) + [数据科学和人工智能技术笔记](https://apachecn.github.io/ds-ai-tech-notes) + [数据科学 IPython 笔记本📚](https://apachecn.github.io/ds-ipynb-zh) + [UCB DS100 数据科学的原理与技巧🚧](https://apachecn.github.io/ds100-textbook-zh) + [ApacheCN 数据科学和人工智能知识库](https://apachecn.github.io/dsai-wiki) + [Matplotlib 用户指南](https://apachecn.github.io/matplotlib-doc-zh) + [MIT 18.06 线性代数笔记](https://apachecn.github.io/mit-18.06-linalg-notes) + [利用 Python 进行数据分析 · 第 2 版](https://apachecn.github.io/pyda-2e-zh) + [QuantLearning](https://apachecn.github.io/quant-learning) + [seaborn 0.9 中文文档](https://apachecn.github.io/seaborn-doc-zh) + [社交媒体挖掘 - 翻译版](https://apachecn.github.io/socialmediamining-zh) + [斯坦福 Stats60 21 世纪的统计思维🚧](https://apachecn.github.io/stats-thinking-21-zh) + [复杂性思维 中文第二版](https://apachecn.github.io/think-comp-2e-zh) + [PyMiner 开发者指南](https://apachecn.github.io/pyminer-dev-guide) + 人工智能 + [JavaTPoint 数据科学与人工智能中文教程📚](https://apachecn.github.io/javatpoint-dsai-zh) + [GeeksForGeeks 人工智能中文教程📚](https://apachecn.github.io/geeksforgeeks-ai-zh) + [AILearning📚](https://apachecn.github.io/ailearning) + [ApacheCN 计算机视觉译文集📚](https://apachecn.github.io/apachecn-cv-zh) + [ApacheCN 计算机视觉译文集(二)📚](https://apachecn.github.io/apachecn-cv-zh-pt2) + [ApacheCN 深度学习译文集📚](https://apachecn.github.io/apachecn-dl-zh) + [ApacheCN 深度学习译文集(二)📚](https://apachecn.github.io/apachecn-dl-zh-pt2) + [ApacheCN 深度学习译文集(三)📚](https://apachecn.github.io/apachecn-dl-zh-pt3) + [ApacheCN 机器学习译文集📚](https://apachecn.github.io/apachecn-ml-zh) + [ApacheCN 机器学习译文集(二)📚](https://apachecn.github.io/apachecn-ml-zh-pt2) + [ApacheCN 机器学习译文集(三)📚](https://apachecn.github.io/apachecn-ml-zh-pt3) + [FastText 中文文档](https://apachecn.github.io/fasttext-doc-zh) + [面向机器学习的特征工程](https://apachecn.github.io/fe4ml-zh) + [Gensim 中文文档](https://apachecn.github.io/gensim-doc-zh) + [Sklearn 与 TensorFlow 机器学习实用指南第二版](https://apachecn.github.io/hands-on-ml-2e-zh) + [LightGBM 中文文档](https://apachecn.github.io/lightgbm-doc-zh) + [Machine Learning Mastery 博客文章翻译📚🚧](https://apachecn.github.io/ml-mastery-zh) + [Machine Learning Mastery 博客文章翻译(二)📚🚧](https://apachecn.github.io/ml-mastery-zh-pt2) + [Python 自然语言处理 第二版](https://apachecn.github.io/nlp-py-2e-zh) + [PyTorch 自然语言处理](https://apachecn.github.io/nlp-pytorch-zh) + [台湾大学林轩田机器学习笔记](https://apachecn.github.io/ntu-hsuantienlin-ml) + [OpenCV 中文文档 4.0.0](https://apachecn.github.io/opencv-doc-zh) + [PythonProgramming.net 系列教程📚](https://apachecn.github.io/python-programming-net-zh) + [PyTorch 中文教程](https://apachecn.github.io/pytorch-doc-zh) + [scikit-learn (sklearn) 官方文档中文版](https://apachecn.github.io/sklearn-doc-zh) + [XGBoost 中文文档](https://apachecn.github.io/xgboost-doc-zh) + 计算机科学 + [JavaTPoint 计算机科学中文教程📚](https://apachecn.github.io/javatpoint-cs-zh) + [ApacheCN 数据结构与算法译文集📚](https://apachecn.github.io/apachecn-algo-zh) + [ApacheCN 计算机系统译文集📚](https://apachecn.github.io/apachecn-sys-zh) + [NUS CS1101s SICP JavaScript 描述🚧](https://apachecn.github.io/sicp-js-zh) + [UCB CS61a SICP Python 描述](https://apachecn.github.io/sicp-py-zh) + [数据结构思维中文版](https://apachecn.github.io/think-dast-zh) + [UIUC CS241 系统编程中文讲义🚧](https://apachecn.github.io/uiuc-cs241-notes-zh) + 安全 + [ApacheCN Kali Linux 译文集📚](https://apachecn.github.io/apachecn-kali-zh) + [ApacheCN 网络安全译文集📚](https://apachecn.github.io/apachecn-sec-zh) + [ApacheCN 网络安全译文集(二)📚](https://apachecn.github.io/apachecn-sec-zh-pt2) + [SecLearning——零组文库备份📚](https://apachecn.github.io/sec-learning) + [ApacheCN 安全知识库📚](https://apachecn.github.io/sec-wiki) + [Web Hacking 101 中文版](https://apachecn.github.io/web-hacking-101-zh) + 其它 + [生化环材劝退文集](https://apachecn.github.io/bio-chem-env-mat-discourage) + [5 分钟商学院精细笔记](https://apachecn.github.io/business-5min-notes) + [iBooker 布客](https://apachecn.github.io/home) + [iBooker 布客老实人报](https://apachecn.github.io/ibooker-plain-dealer) + [使用 Qiskit 学习量子计算 - 翻译版](https://apachecn.github.io/lqcuq-zh) + [原则 · 中文版](https://apachecn.github.io/principles-zh) + [斯坦福 CS183 & YC 创业课系列中文笔记📚](https://apachecn.github.io/stanford-cs183-notes) + [iBooker 团队知识库📚](https://apachecn.github.io/team-wiki) + [ApacheCN 技术评论](https://apachecn.github.io/tech-review) + [通往财富自由之路精细笔记](https://apachecn.github.io/the-way-to-wealth-freedom-notes) ================================================ FILE: README.md ================================================ # ApacheCN C/C++ 译文集 > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) > > 让开源界提前感受入关。 * [在线阅读](https://ccpp.apachecn.org) * [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-c-cpp-zh/) * [ApacheCN 学习资源](http://docs.apachecn.org/) ## 目录 + [笨办法学C 中文版](docs/lcthw-zh/SUMMARY.md) + [C++ 高级编程](docs/adv-cpp/SUMMARY.md) + [C++ 高级编程秘籍](docs/adv-cpp-prog-cb/SUMMARY.md) + [C++ Qt5 GUI 编程](docs/cpp-gui-prog-qt5/SUMMARY.md) + [C++ 专家级编程](docs/exp-cpp/SUMMARY.md) + [C++ 游戏动画编程实用指南](docs/handson-cpp-game-ani-prog/SUMMARY.md) + [C++ 函数式编程实用指南](docs/handson-func-prog-cpp/SUMMARY.md) + [C++ 机器学习实用指南](docs/handson-ml-cpp/SUMMARY.md) + [通过使用 UE4 构建游戏学习 C++](docs/learn-cpp-build-game-ue4/SUMMARY.md) + [精通 C++ 游戏开发](docs/master-cpp-game-dev/SUMMARY.md) + [精通 C++ 编程](docs/master-cpp-prog/SUMMARY.md) + [Qt5 C++ GUI 编程秘籍](docs/qt5-cpp-gui-prog-cb/SUMMARY.md) + [Qt Creator 应用开发](docs/app-dev-qt-creator/SUMMARY.md) + [C++ 编程入门手册](docs/begin-cpp-prog/SUMMARY.md) + [现代 C++ 嵌入式编程秘籍](docs/emb-prog-mod-cpp-cb/SUMMARY.md) + [C++ 专家级编程](docs/exp-cpp-prog/SUMMARY.md) + [UE 游戏开发项目](docs/game-dev-proj-ue/SUMMARY.md) + [CUDA 编程学习手册](docs/learn-cuda-prog/SUMMARY.md) + [WebAssembly 学习手册](docs/learn-wasm/SUMMARY.md) + [精通 C++ 多线程](docs/master-cpp-multithrd/SUMMARY.md) + [现代 C++ 编程](docs/mod-cpp/SUMMARY.md) + [现代 C++ 的挑战](docs/mod-cpp-challenge/SUMMARY.md) + [C++ 游戏编程入门手册](docs/begin-cpp-game-prog/SUMMARY.md) + [Boost.Asio C++ 网络编程入门中文第二版](docs/boost-asio-cpp-net-prog-2e/SUMMARY.md) + [Boost C++ 应用开发秘籍](docs/boost-cpp-app-dev-cb/SUMMARY.md) + [C++ 数据结构和算法设计原则](docs/cpp-dsal-design-principle/SUMMARY.md) + [C++ 高性能编程](docs/cpp-hiperf/SUMMARY.md) + [C++ 反应式编程](docs/cpp-react-prog/SUMMARY.md) + [C++ 系统编程秘籍](docs/cpp-sys-prog-cb/SUMMARY.md) + [C++ 工作室](docs/cpp-workshop/SUMMARY.md) + [WebAssembly 游戏编程实用指南](docs/handson-game-dev-wasm/SUMMARY.md) + [C++ 函数式编程学习手册](docs/learn-cpp-func-prog/SUMMARY.md) + [Qt5 学习手册](docs/learn-qt5/SUMMARY.md) ## 贡献指南 为了不断改进翻译质量,我们特此启动了【翻译、校对、笔记整理活动】,开设了多个校对项目。贡献者校对一章之后可以领取千字2\~4元的奖励。进行中的校对活动请见[活动列表](https://home.apachecn.org/#/docs/activity/docs-activity)。更多详情请联系飞龙(Q562826179,V:wizardforcel)。 ## 联系方式 ### 负责人 * [飞龙](https://github.com/wizardforcel): 562826179 ### 其他 * 在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue. * 发邮件到 Email: `apachecn@163.com`. * 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. ## 下载 ### Docker ``` docker pull apachecn0/apachecn-c-cpp-zh docker run -tid -p :80 apachecn0/apachecn-c-cpp-zh # 访问 http://localhost:{port} 查看文档 ``` ### PYPI ``` pip install apachecn-c-cpp-zh apachecn-c-cpp-zh # 访问 http://localhost:{port} 查看文档 ``` ### NPM ``` npm install -g apachecn-c-cpp-zh apachecn-c-cpp-zh # 访问 http://localhost:{port} 查看文档 ``` ## 赞助我们 ![](http://data.apachecn.org/img/about/donate.jpg) ================================================ FILE: SUMMARY.md ================================================ + [笨办法学C 中文版](docs/lcthw-zh/README.md) + [前言](docs/lcthw-zh/preface.md) + [导言:C的笛卡尔之梦](docs/lcthw-zh/introduction.md) + [练习0:准备](docs/lcthw-zh/ex0.md) + [练习1:启用编译器](docs/lcthw-zh/ex1.md) + [练习2:用Make来代替Python](docs/lcthw-zh/ex2.md) + [练习3:格式化输出](docs/lcthw-zh/ex3.md) + [练习4:Valgrind 介绍](docs/lcthw-zh/ex4.md) + [练习5:一个C程序的结构](docs/lcthw-zh/ex5.md) + [练习6:变量类型](docs/lcthw-zh/ex6.md) + [练习7:更多变量和一些算术](docs/lcthw-zh/ex7.md) + [练习8:大小和数组](docs/lcthw-zh/ex8.md) + [练习9:数组和字符串](docs/lcthw-zh/ex9.md) + [练习10:字符串数组和循环](docs/lcthw-zh/ex10.md) + [练习11:While循环和布尔表达式](docs/lcthw-zh/ex11.md) + [练习12:If,Else If,Else](docs/lcthw-zh/ex12.md) + [练习13:Switch语句](docs/lcthw-zh/ex13.md) + [练习14:编写并使用函数](docs/lcthw-zh/ex14.md) + [练习15:指针,可怕的指针](docs/lcthw-zh/ex15.md) + [练习16:结构体和指向它们的指针](docs/lcthw-zh/ex16.md) + [练习17:堆和栈的内存分配](docs/lcthw-zh/ex17.md) + [练习18:函数指针](docs/lcthw-zh/ex18.md) + [练习19:一个简单的对象系统](docs/lcthw-zh/ex19.md) + [练习20:Zed的强大的调试宏](docs/lcthw-zh/ex20.md) + [练习21:高级数据类型和控制结构](docs/lcthw-zh/ex21.md) + [练习22:栈、作用域和全局](docs/lcthw-zh/ex22.md) + [练习23:认识达夫设备](docs/lcthw-zh/ex23.md) + [练习24:输入输出和文件](docs/lcthw-zh/ex24.md) + [练习25:变参函数](docs/lcthw-zh/ex25.md) + [练习26:编写第一个真正的程序](docs/lcthw-zh/ex26.md) + [练习27:创造性和防御性编程](docs/lcthw-zh/ex27.md) + [练习28:Makefile 进阶](docs/lcthw-zh/ex28.md) + [练习29:库和链接](docs/lcthw-zh/ex29.md) + [练习30:自动化测试](docs/lcthw-zh/ex30.md) + [练习31:代码调试](docs/lcthw-zh/ex31.md) + [练习32:双向链表](docs/lcthw-zh/ex32.md) + [练习33:链表算法](docs/lcthw-zh/ex33.md) + [练习34:动态数组](docs/lcthw-zh/ex34.md) + [练习35:排序和搜索](docs/lcthw-zh/ex35.md) + [练习36:更安全的字符串](docs/lcthw-zh/ex36.md) + [练习37:哈希表](docs/lcthw-zh/ex37.md) + [练习38:哈希算法](docs/lcthw-zh/ex38.md) + [练习39:字符串算法](docs/lcthw-zh/ex39.md) + [练习40:二叉搜索树](docs/lcthw-zh/ex40.md) + [练习41:将 Cachegrind 和 Callgrind 用于性能调优](docs/lcthw-zh/ex41.md) + [练习42:栈和队列](docs/lcthw-zh/ex42.md) + [练习43:一个简单的统计引擎](docs/lcthw-zh/ex43.md) + [练习44:环形缓冲区](docs/lcthw-zh/ex44.md) + [练习45:一个简单的TCP/IP客户端](docs/lcthw-zh/ex45.md) + [练习46:三叉搜索树](docs/lcthw-zh/ex46.md) + [练习47:一个快速的URL路由](docs/lcthw-zh/ex47.md) + [后记:“解构 K&R C” 已死](docs/lcthw-zh/postscript.md) + [捐赠名单](docs/lcthw-zh/donors.md) + [C++ 高级编程](docs/adv-cpp/README.md) + [零、前言](docs/adv-cpp/00.md) + [一、可移植的 C++ 软件剖析](docs/adv-cpp/01.md) + [二、不允许鸭子——类型和推导(一)](docs/adv-cpp/02.md) + [三、不允许鸭子——模板和推导(二)](docs/adv-cpp/03.md) + [四、不允许泄漏——异常和资源](docs/adv-cpp/04.md) + [五、关注点分离——软件架构、函数和可变模板](docs/adv-cpp/05.md) + [六、哲学家的晚餐——线程和并发](docs/adv-cpp/06.md) + [七、流和输入/输出](docs/adv-cpp/07.md) + [八、每个人都会跌倒,这是你爬起来的方式——测试和调试](docs/adv-cpp/08.md) + [九、对速度的需求——性能和优化](docs/adv-cpp/09.md) + [十、附录](docs/adv-cpp/10.md) + [C++ 高级编程秘籍](docs/adv-cpp-prog-cb/README.md) + [零、前言](docs/adv-cpp-prog-cb/00.md) + [一、库的开发入门](docs/adv-cpp-prog-cb/01.md) + [二、将异常用于错误处理](docs/adv-cpp-prog-cb/02.md) + [三、实现移动语义](docs/adv-cpp-prog-cb/03.md) + [四、将模板用于泛型编程](docs/adv-cpp-prog-cb/04.md) + [五、并发和同步](docs/adv-cpp-prog-cb/05.md) + [六、优化代码以提高性能](docs/adv-cpp-prog-cb/06.md) + [七、调试和测试](docs/adv-cpp-prog-cb/07.md) + [八、创建和实现您自己的容器](docs/adv-cpp-prog-cb/08.md) + [九、探索类型擦除](docs/adv-cpp-prog-cb/09.md) + [十、对动态分配的深入研究](docs/adv-cpp-prog-cb/10.md) + [十一、C++ 中的常见模式](docs/adv-cpp-prog-cb/11.md) + [十二、更仔细查看类型推导](docs/adv-cpp-prog-cb/12.md) + [十三、奖励——使用 C++ 20 特性](docs/adv-cpp-prog-cb/13.md) + [C++ Qt5 GUI 编程](docs/cpp-gui-prog-qt5/README.md) + [零、前言](docs/cpp-gui-prog-qt5/00.md) + [一、Qt 简介](docs/cpp-gui-prog-qt5/01.md) + [二、Qt 小部件和样式表](docs/cpp-gui-prog-qt5/02.md) + [三、数据库连接](docs/cpp-gui-prog-qt5/03.md) + [四、图表](docs/cpp-gui-prog-qt5/04.md) + [五、项目视图和对话框](docs/cpp-gui-prog-qt5/05.md) + [六、整合网络内容](docs/cpp-gui-prog-qt5/06.md) + [七、地图查看器](docs/cpp-gui-prog-qt5/07.md) + [八、图形视图](docs/cpp-gui-prog-qt5/08.md) + [九、照相机模块](docs/cpp-gui-prog-qt5/09.md) + [十、即时消息](docs/cpp-gui-prog-qt5/10.md) + [十一、实现图形编辑器](docs/cpp-gui-prog-qt5/11.md) + [十二、云存储](docs/cpp-gui-prog-qt5/12.md) + [十三、多媒体查看器](docs/cpp-gui-prog-qt5/13.md) + [十四、Qt Quick 和 QML](docs/cpp-gui-prog-qt5/14.md) + [十五、跨平台开发](docs/cpp-gui-prog-qt5/15.md) + [十六、测试和调试](docs/cpp-gui-prog-qt5/16.md) + [C++ 专家级编程](docs/exp-cpp/README.md) + [零、前言](docs/exp-cpp/00.md) + [第一部分:C++ 编程的背后](docs/exp-cpp/sec1.md) + [一、构建 C++ 应用简介](docs/exp-cpp/01.md) + [二、C++ 低级编程](docs/exp-cpp/02.md) + [三、面向对象编程的细节](docs/exp-cpp/03.md) + [四、理解和设计模板](docs/exp-cpp/04.md) + [五、内存管理和智能指针](docs/exp-cpp/05.md) + [第二部分:设计健壮高效的应用](docs/exp-cpp/sec2.md) + [六、STL 中数据结构和算法的挖掘](docs/exp-cpp/06.md) + [七、函数式编程](docs/exp-cpp/07.md) + [八、并发和多线程](docs/exp-cpp/08.md) + [九、设计并发数据结构](docs/exp-cpp/09.md) + [十、设计全球通用的应用](docs/exp-cpp/10.md) + [十一、使用设计模式设计策略游戏](docs/exp-cpp/11.md) + [十二、网络和安全](docs/exp-cpp/12.md) + [十三、调试和测试](docs/exp-cpp/13.md) + [十四、使用 Qt 的图形用户界面](docs/exp-cpp/14.md) + [第三部分:人工智能世界中的 C++](docs/exp-cpp/sec3.md) + [十五、C++ 在机器学习任务中的应用](docs/exp-cpp/15.md) + [十六、实现基于对话的搜索引擎](docs/exp-cpp/16.md) + [十七、答案](docs/exp-cpp/17.md) + [C++ 游戏动画编程实用指南](docs/handson-cpp-game-ani-prog/README.md) + [零、前言](docs/handson-cpp-game-ani-prog/00.md) + [一、创建游戏窗口](docs/handson-cpp-game-ani-prog/01.md) + [二、实现向量](docs/handson-cpp-game-ani-prog/02.md) + [三、实现矩阵](docs/handson-cpp-game-ani-prog/03.md) + [四、实现四元数](docs/handson-cpp-game-ani-prog/04.md) + [五、实现转换](docs/handson-cpp-game-ani-prog/05.md) + [六、构建抽象渲染器](docs/handson-cpp-game-ani-prog/06.md) + [七、探索 glTF 文件格式](docs/handson-cpp-game-ani-prog/07.md) + [八、创建曲线、帧和轨迹](docs/handson-cpp-game-ani-prog/08.md) + [九、实现动画剪辑](docs/handson-cpp-game-ani-prog/09.md) + [十、网格蒙皮](docs/handson-cpp-game-ani-prog/10.md) + [十一、优化动画流水线](docs/handson-cpp-game-ani-prog/11.md) + [十二、动画之间的融合](docs/handson-cpp-game-ani-prog/12.md) + [十三、实现逆运动学](docs/handson-cpp-game-ani-prog/13.md) + [十四、使用对偶四元数蒙皮](docs/handson-cpp-game-ani-prog/14.md) + [十五、使用实例渲染人群](docs/handson-cpp-game-ani-prog/15.md) + [C++ 函数式编程实用指南](docs/handson-func-prog-cpp/README.md) + [零、前言](docs/handson-func-prog-cpp/00.md) + [第一部分:C++ 中的函数组件](docs/handson-func-prog-cpp/sec1.md) + [一、函数式编程导论](docs/handson-func-prog-cpp/01.md) + [二、理解纯函数](docs/handson-func-prog-cpp/02.md) + [三、深入 lambdas](docs/handson-func-prog-cpp/03.md) + [四、函数组合思想](docs/handson-func-prog-cpp/04.md) + [五、局部应用与柯里化](docs/handson-func-prog-cpp/05.md) + [第二部分:函数设计](docs/handson-func-prog-cpp/sec2.md) + [六、函数思维——从数据输入到数据输出](docs/handson-func-prog-cpp/06.md) + [七、通过函数操作消除重复](docs/handson-func-prog-cpp/07.md) + [八、使用类提高内聚性](docs/handson-func-prog-cpp/08.md) + [九、面向函数式编程的测试驱动开发](docs/handson-func-prog-cpp/09.md) + [第三部分:收获函数式编程的好处](docs/handson-func-prog-cpp/sec3.md) + [十、性能优化](docs/handson-func-prog-cpp/10.md) + [十一、基于属性的测试](docs/handson-func-prog-cpp/11.md) + [十二、重构到纯函数和通过纯函数重构](docs/handson-func-prog-cpp/12.md) + [十三、不变性和架构——事件源](docs/handson-func-prog-cpp/13.md) + [第四部分:C++ 函数式编程的现状和未来](docs/handson-func-prog-cpp/sec4.md) + [十四、使用范围库的延迟求值](docs/handson-func-prog-cpp/14.md) + [十五、STL 支持和建议](docs/handson-func-prog-cpp/15.md) + [十六、标准语言支持和建议](docs/handson-func-prog-cpp/16.md) + [十七、答案](docs/handson-func-prog-cpp/17.md) + [通过使用 UE4 构建游戏学习 C++](docs/learn-cpp-build-game-ue4/README.md) + [零、前言](docs/learn-cpp-build-game-ue4/00.md) + [一、C++ 17 入门](docs/learn-cpp-build-game-ue4/01.md) + [二、变量和内存](docs/learn-cpp-build-game-ue4/02.md) + [三、`if...else`和`switch`](docs/learn-cpp-build-game-ue4/03.md) + [四、循环](docs/learn-cpp-build-game-ue4/04.md) + [五、函数和宏](docs/learn-cpp-build-game-ue4/05.md) + [六、对象、类和继承](docs/learn-cpp-build-game-ue4/06.md) + [七、动态存储分配](docs/learn-cpp-build-game-ue4/07.md) + [八、演员和棋子](docs/learn-cpp-build-game-ue4/08.md) + [九、模板和常用容器](docs/learn-cpp-build-game-ue4/09.md) + [十、库存系统和提取项目](docs/learn-cpp-build-game-ue4/10.md) + [十一、怪物](docs/learn-cpp-build-game-ue4/11.md) + [十二、使用高级人工智能构建更聪明的怪物](docs/learn-cpp-build-game-ue4/12.md) + [十三、咒语书](docs/learn-cpp-build-game-ue4/13.md) + [十四、利用 UMG 和音频改善用户界面反馈](docs/learn-cpp-build-game-ue4/14.md) + [十五、虚拟现实及其他](docs/learn-cpp-build-game-ue4/15.md) + [精通 C++ 游戏开发](docs/master-cpp-game-dev/README.md) + [零、前言](docs/master-cpp-game-dev/00.md) + [一、面向游戏开发的 C++ 语言](docs/master-cpp-game-dev/01.md) + [二、理解库](docs/master-cpp-game-dev/02.md) + [三、夯实基础](docs/master-cpp-game-dev/03.md) + [四、构建素材管道](docs/master-cpp-game-dev/04.md) + [五、构建游戏系统](docs/master-cpp-game-dev/05.md) + [六、创建图形用户界面](docs/master-cpp-game-dev/06.md) + [七、高级渲染](docs/master-cpp-game-dev/07.md) + [八、高级游戏系统](docs/master-cpp-game-dev/08.md) + [九、人工智能](docs/master-cpp-game-dev/09.md) + [十、多个玩家](docs/master-cpp-game-dev/10.md) + [十一、虚拟现实](docs/master-cpp-game-dev/11.md) + [精通 C++ 编程](docs/master-cpp-prog/README.md) + [零、前言](docs/master-cpp-prog/00.md) + [一、C++ 17 特性](docs/master-cpp-prog/01.md) + [二、标准模板库](docs/master-cpp-prog/02.md) + [三、模板编程](docs/master-cpp-prog/03.md) + [四、智能指针](docs/master-cpp-prog/04.md) + [五、使用 C++ 开发图形用户界面应用](docs/master-cpp-prog/05.md) + [六、多线程编程和进程间通信](docs/master-cpp-prog/06.md) + [七、测试驱动开发](docs/master-cpp-prog/07.md) + [八、行为驱动开发](docs/master-cpp-prog/08.md) + [九、调试技术](docs/master-cpp-prog/09.md) + [十、代码异味和整洁的代码实践](docs/master-cpp-prog/10.md) + [Qt5 C++ GUI 编程秘籍](docs/qt5-cpp-gui-prog-cb/README.md) + [零、前言](docs/qt5-cpp-gui-prog-cb/00.md) + [一、将 Qt 设计器用于外观定制](docs/qt5-cpp-gui-prog-cb/01.md) + [二、事件处理——信号和插槽](docs/qt5-cpp-gui-prog-cb/02.md) + [三、Qt 和 QML 的状态和动画](docs/qt5-cpp-gui-prog-cb/03.md) + [四、画家与 2D 图形](docs/qt5-cpp-gui-prog-cb/04.md) + [五、OpenGL 实现](docs/qt5-cpp-gui-prog-cb/05.md) + [六、使用网络和管理大型文档](docs/qt5-cpp-gui-prog-cb/06.md) + [七、线程基础——异步编程](docs/qt5-cpp-gui-prog-cb/07.md) + [八、使用 Qt5 构建触摸屏应用](docs/qt5-cpp-gui-prog-cb/08.md) + [九、XML 解析的简化](docs/qt5-cpp-gui-prog-cb/09.md) + [十、转换库](docs/qt5-cpp-gui-prog-cb/10.md) + [十一、使用 SQL 驱动和 Qt 访问数据库](docs/qt5-cpp-gui-prog-cb/11.md) + [十二、使用 Qt 网络引擎开发网络应用](docs/qt5-cpp-gui-prog-cb/12.md) + [十三、性能优化](docs/qt5-cpp-gui-prog-cb/13.md) + [Qt Creator 应用开发](docs/app-dev-qt-creator/README.md) + [零、前言](docs/app-dev-qt-creator/00.md) + [第一部分:基础知识](docs/app-dev-qt-creator/sec1.md) + [一、Qt Creator 入门](docs/app-dev-qt-creator/01.md) + [二、使用 Qt Creator 构建应用](docs/app-dev-qt-creator/02.md) + [三、使用 Qt Designer 设计应用](docs/app-dev-qt-creator/03.md) + [四、Qt 基础](docs/app-dev-qt-creator/04.md) + [五、使用 Qt 小部件开发应用](docs/app-dev-qt-creator/05.md) + [第二部分:高级功能](docs/app-dev-qt-creator/sec2.md) + [六、使用 Qt 绘图](docs/app-dev-qt-creator/06.md) + [七、使用 Qt Quick 实现更多功能](docs/app-dev-qt-creator/07.md) + [八、使用 Qt Quick 实现多媒体](docs/app-dev-qt-creator/08.md) + [九、传感器和 Qt Quick](docs/app-dev-qt-creator/09.md) + [第三部分:实际事项](docs/app-dev-qt-creator/sec3.md) + [十、使用 Qt 语言学家本地化您的应用](docs/app-dev-qt-creator/10.md) + [十一、使用 Qt Creator 优化性能](docs/app-dev-qt-creator/11.md) + [十二、使用 Qt Creator 开发移动应用](docs/app-dev-qt-creator/12.md) + [十三、使用 Qt Creator 开发嵌入式和物联网](docs/app-dev-qt-creator/13.md) + [十四、QT 提示和技巧](docs/app-dev-qt-creator/14.md) + [C++ 编程入门手册](docs/begin-cpp-prog/README.md) + [零、前言](docs/begin-cpp-prog/00.md) + [一、从 C++ 开始](docs/begin-cpp-prog/01.md) + [二、了解语言特性](docs/begin-cpp-prog/02.md) + [三、探索 C++ 类型](docs/begin-cpp-prog/03.md) + [四、使用内存、数组和指针](docs/begin-cpp-prog/04.md) + [五、使用函数](docs/begin-cpp-prog/05.md) + [六、类](docs/begin-cpp-prog/06.md) + [七、面向对象编程导论](docs/begin-cpp-prog/07.md) + [八、使用标准库容器](docs/begin-cpp-prog/08.md) + [九、使用字符串](docs/begin-cpp-prog/09.md) + [十、诊断和调试](docs/begin-cpp-prog/10.md) + [现代 C++ 嵌入式编程秘籍](docs/emb-prog-mod-cpp-cb/README.md) + [零、前言](docs/emb-prog-mod-cpp-cb/00.md) + [一、嵌入式系统基础](docs/emb-prog-mod-cpp-cb/01.md) + [二、设置环境](docs/emb-prog-mod-cpp-cb/02.md) + [三、使用不同的架构](docs/emb-prog-mod-cpp-cb/03.md) + [四、处理中断](docs/emb-prog-mod-cpp-cb/04.md) + [五、调试、日志记录和性能分析](docs/emb-prog-mod-cpp-cb/05.md) + [六、内存管理](docs/emb-prog-mod-cpp-cb/06.md) + [七、多线程和同步](docs/emb-prog-mod-cpp-cb/07.md) + [八、通信和序列化](docs/emb-prog-mod-cpp-cb/08.md) + [九、外部设备](docs/emb-prog-mod-cpp-cb/09.md) + [十、降低功耗](docs/emb-prog-mod-cpp-cb/10.md) + [十一、时间点和间隔](docs/emb-prog-mod-cpp-cb/11.md) + [十二、错误处理和容错](docs/emb-prog-mod-cpp-cb/12.md) + [十三、实时系统指南](docs/emb-prog-mod-cpp-cb/13.md) + [十四、安全关键系统指南](docs/emb-prog-mod-cpp-cb/14.md) + [十五、微控制器编程](docs/emb-prog-mod-cpp-cb/15.md) + [C++ 专家级编程](docs/exp-cpp-prog/README.md) + [零、新的 C++ 17 特性](docs/exp-cpp-prog/00.md) + [一、容器](docs/exp-cpp-prog/01.md) + [二、迭代器](docs/exp-cpp-prog/02.md) + [三、lambda 表达式](docs/exp-cpp-prog/03.md) + [四、STL 算法基础](docs/exp-cpp-prog/04.md) + [五、STL 算法的高级使用](docs/exp-cpp-prog/05.md) + [六、字符串、流类和正则表达式](docs/exp-cpp-prog/06.md) + [七、工具类](docs/exp-cpp-prog/07.md) + [八、并行性和并发性](docs/exp-cpp-prog/08.md) + [九、文件系统](docs/exp-cpp-prog/09.md) + [UE 游戏开发项目](docs/game-dev-proj-ue/README.md) + [零、前言](docs/game-dev-proj-ue/00.md) + [一、虚幻引擎介绍](docs/game-dev-proj-ue/01.md) + [二、使用虚幻引擎](docs/game-dev-proj-ue/02.md) + [三、角色类组件和蓝图设置](docs/game-dev-proj-ue/03.md) + [四、玩家输入](docs/game-dev-proj-ue/04.md) + [五、线条痕迹](docs/game-dev-proj-ue/05.md) + [六、碰撞物体](docs/game-dev-proj-ue/06.md) + [八、用户界面](docs/game-dev-proj-ue/07.md) + [九、视听元素](docs/game-dev-proj-ue/08.md) + [十、创建`SuperSideScroller`游戏](docs/game-dev-proj-ue/09.md) + [十一、混合空间 1D、按键绑定和状态机](docs/game-dev-proj-ue/10.md) + [十二、动画混合和蒙太奇](docs/game-dev-proj-ue/11.md) + [十三、敌方人工智能](docs/game-dev-proj-ue/12.md) + [十四、产生玩家投射物](docs/game-dev-proj-ue/13.md) + [十五、收藏品、加强和拾取](docs/game-dev-proj-ue/14.md) + [十六、多人游戏基础](docs/game-dev-proj-ue/15.md) + [十七、远程过程调用](docs/game-dev-proj-ue/16.md) + [十八、多人游戏中的游戏框架类](docs/game-dev-proj-ue/17.md) + [CUDA 编程学习手册](docs/learn-cuda-prog/README.md) + [零、前言](docs/learn-cuda-prog/00.md) + [一、CUDA 编程入门](docs/learn-cuda-prog/01.md) + [二、内存管理](docs/learn-cuda-prog/02.md) + [三、线程编程](docs/learn-cuda-prog/03.md) + [四、内核执行模型及优化策略](docs/learn-cuda-prog/04.md) + [五、应用分析和调试](docs/learn-cuda-prog/05.md) + [六、可扩展的多图形处理器编程](docs/learn-cuda-prog/06.md) + [七、CUDA 中的并行编程模式](docs/learn-cuda-prog/07.md) + [八、使用库和其他语言编程](docs/learn-cuda-prog/08.md) + [八、将 OpenACC 用于图形处理器编程](docs/learn-cuda-prog/09.md) + [九、利用 CUDA 实现深度学习加速](docs/learn-cuda-prog/10.md) + [十一、附录](docs/learn-cuda-prog/11.md) + [WebAssembly 学习手册](docs/learn-wasm/README.md) + [零、前言](docs/learn-wasm/00.md) + [一、什么是 WebAssembly?](docs/learn-wasm/01.md) + [二、WebAssembly 的元素——Wat、Wasm 和 JavaScript 应用编程接口](docs/learn-wasm/02.md) + [三、建立开发环境](docs/learn-wasm/03.md) + [四、安装所需的依赖项](docs/learn-wasm/04.md) + [五、创建和加载 WebAssembly 模块](docs/learn-wasm/05.md) + [六、与 JavaScript 交互和调试](docs/learn-wasm/06.md) + [七、从头开始创建应用](docs/learn-wasm/07.md) + [八、使用电子脚本移植游戏](docs/learn-wasm/08.md) + [九、与 Node.js 集成](docs/learn-wasm/09.md) + [十、高级工具和即将推出的功能](docs/learn-wasm/10.md) + [精通 C++ 多线程](docs/master-cpp-multithrd/README.md) + [零、前言](docs/master-cpp-multithrd/00.md) + [一、重温多线程](docs/master-cpp-multithrd/01.md) + [二、处理器和操作系统上的多线程实现](docs/master-cpp-multithrd/02.md) + [三、C++ 多线程应用编程接口](docs/master-cpp-multithrd/03.md) + [四、线程同步和通信](docs/master-cpp-multithrd/04.md) + [五、本机 C++ 线程和原语](docs/master-cpp-multithrd/05.md) + [六、调试多线程代码](docs/master-cpp-multithrd/06.md) + [七、最佳实践](docs/master-cpp-multithrd/07.md) + [八、原子操作——使用硬件](docs/master-cpp-multithrd/08.md) + [九、分布式计算中的多线程](docs/master-cpp-multithrd/09.md) + [十、图形处理器多线程](docs/master-cpp-multithrd/10.md) + [现代 C++ 编程](docs/mod-cpp/README.md) + [零、前言](docs/mod-cpp/00.md) + [一、理解语言特性](docs/mod-cpp/01.md) + [二、使用内存、数组和指针](docs/mod-cpp/02.md) + [三、使用函数](docs/mod-cpp/03.md) + [四、类](docs/mod-cpp/04.md) + [五、使用标准库容器](docs/mod-cpp/05.md) + [六、使用字符串](docs/mod-cpp/06.md) + [七、诊断和调试](docs/mod-cpp/07.md) + [八、学习现代核心语言特性](docs/mod-cpp/08.md) + [九、使用数字和字符串](docs/mod-cpp/09.md) + [十、探索函数](docs/mod-cpp/10.md) + [十一、标准库容器、算法和迭代器](docs/mod-cpp/11.md) + [十二、数学问题](docs/mod-cpp/12.md) + [十三、语言特性](docs/mod-cpp/13.md) + [十四、字符串和正则表达式](docs/mod-cpp/14.md) + [十五、流和文件系统](docs/mod-cpp/15.md) + [十六、日期和时间](docs/mod-cpp/16.md) + [十七、算法和数据结构](docs/mod-cpp/17.md) + [现代 C++ 的挑战](docs/mod-cpp-challenge/README.md) + [零、前言](docs/mod-cpp-challenge/00.md) + [一、数学问题](docs/mod-cpp-challenge/01.md) + [二、语言特性](docs/mod-cpp-challenge/02.md) + [三、字符串和正则表达式](docs/mod-cpp-challenge/03.md) + [四、流和文件系统](docs/mod-cpp-challenge/04.md) + [五、日期和时间](docs/mod-cpp-challenge/05.md) + [六、算法和数据结构](docs/mod-cpp-challenge/06.md) + [七、并发](docs/mod-cpp-challenge/07.md) + [八、设计模式](docs/mod-cpp-challenge/08.md) + [九、数据序列化](docs/mod-cpp-challenge/09.md) + [十、归档、图像和数据库](docs/mod-cpp-challenge/10.md) + [十一、密码系统](docs/mod-cpp-challenge/11.md) + [十二、网络和服务](docs/mod-cpp-challenge/12.md) + [十三、参考文献](docs/mod-cpp-challenge/13.md) + [C++ 游戏编程入门手册](docs/begin-cpp-game-prog/README.md) + [零、序言](docs/begin-cpp-game-prog/00.md) + [一、C++,SFML,VisualStudio,并开始第一个游戏](docs/begin-cpp-game-prog/01.md) + [二、变量、运算符和决策——设置精灵动画](docs/begin-cpp-game-prog/02.md) + [三、C++ 字符串和 SFML 时间——玩家输入和 HUD](docs/begin-cpp-game-prog/03.md) + [四、循环、数组、`switch`、枚举和函数——实现游戏机制](docs/begin-cpp-game-prog/04.md) + [五、碰撞、声音和结束条件——使游戏可玩](docs/begin-cpp-game-prog/05.md) + [六、面向对象编程——启动乒乓球游戏](docs/begin-cpp-game-prog/06.md) + [七、动态碰撞检测与物理——完成乒乓球游戏](docs/begin-cpp-game-prog/07.md) + [八、SFML 视图——开始僵尸射击游戏](docs/begin-cpp-game-prog/08.md) + [九、C++ 引用、精灵列表和顶点数组](docs/begin-cpp-game-prog/09.md) + [十、指针、标准模板库、纹理管理](docs/begin-cpp-game-prog/10.md) + [十一、碰撞检测,拾音器和子弹](docs/begin-cpp-game-prog/11.md) + [十二、视图分层与 HUD 实现](docs/begin-cpp-game-prog/12.md) + [十三、音效,文件 I/O,完成游戏](docs/begin-cpp-game-prog/13.md) + [十四、抽象和代码管理——更好地利用面向对象](docs/begin-cpp-game-prog/14.md) + [十五、高级 OOP——继承与多态](docs/begin-cpp-game-prog/15.md) + [十六、建造可玩关卡和碰撞检测](docs/begin-cpp-game-prog/16.md) + [十七、声音空间化和平视显示器](docs/begin-cpp-game-prog/17.md) + [十八、粒子系统和着色器](docs/begin-cpp-game-prog/18.md) + [十九、游戏编程设计模式——启动太空入侵者 ++ 游戏](docs/begin-cpp-game-prog/19.md) + [二十、游戏对象和组件](docs/begin-cpp-game-prog/20.md) + [二十一、文件输入输出和游戏对象工厂](docs/begin-cpp-game-prog/21.md) + [二十二、使用游戏对象和构建游戏](docs/begin-cpp-game-prog/22.md) + [二十三、结束之前](docs/begin-cpp-game-prog/23.md) + [Boost.Asio C++ 网络编程入门中文第二版](docs/boost-asio-cpp-net-prog-2e/README.md) + [零、前言](docs/boost-asio-cpp-net-prog-2e/0.md) + [一、使用 C++ 简化您的网络编程](docs/boost-asio-cpp-net-prog-2e/1.md) + [二、理解网络概念](docs/boost-asio-cpp-net-prog-2e/2.md) + [三、Boost C++ 库简介](docs/boost-asio-cpp-net-prog-2e/3.md) + [四、Boost.Asio 入门](docs/boost-asio-cpp-net-prog-2e/4.md) + [五、深入研究 Boost.Asio 库](docs/boost-asio-cpp-net-prog-2e/5.md) + [六、创建客户端——服务器应用](docs/boost-asio-cpp-net-prog-2e/6.md) + [七、调试代码并解决错误](docs/boost-asio-cpp-net-prog-2e/7.md) + [Boost C++ 应用开发秘籍](docs/boost-cpp-app-dev-cb/README.md) + [零、前言](docs/boost-cpp-app-dev-cb/00.md) + [一、开始编写应用](docs/boost-cpp-app-dev-cb/01.md) + [二、管理资源](docs/boost-cpp-app-dev-cb/02.md) + [三、类型转换](docs/boost-cpp-app-dev-cb/03.md) + [四、编译时技巧](docs/boost-cpp-app-dev-cb/04.md) + [五、多线程操作](docs/boost-cpp-app-dev-cb/05.md) + [六、操作任务](docs/boost-cpp-app-dev-cb/06.md) + [七、操纵字符串](docs/boost-cpp-app-dev-cb/07.md) + [八、元编程](docs/boost-cpp-app-dev-cb/08.md) + [九、容器](docs/boost-cpp-app-dev-cb/09.md) + [十、收集平台和编译器信息](docs/boost-cpp-app-dev-cb/10.md) + [十一、使用系统](docs/boost-cpp-app-dev-cb/11.md) + [十二、Boost 的冰山一角](docs/boost-cpp-app-dev-cb/12.md) + [C++ 数据结构和算法设计原则](docs/cpp-dsal-design-principle/README.md) + [零、前言](docs/cpp-dsal-design-principle/00.md) + [一、列表、栈和队列](docs/cpp-dsal-design-principle/01.md) + [二、树、堆和图](docs/cpp-dsal-design-principle/02.md) + [三、哈希表和布隆过滤器](docs/cpp-dsal-design-principle/03.md) + [四、分治法](docs/cpp-dsal-design-principle/04.md) + [五、贪婪算法](docs/cpp-dsal-design-principle/05.md) + [六、图算法 1](docs/cpp-dsal-design-principle/06.md) + [七、图算法 2](docs/cpp-dsal-design-principle/07.md) + [八、动态规划一](docs/cpp-dsal-design-principle/08.md) + [九、动态规划二](docs/cpp-dsal-design-principle/09.md) + [十、附录](docs/cpp-dsal-design-principle/10.md) + [C++ 高性能编程](docs/cpp-hiperf/README.md) + [零、前言](docs/cpp-hiperf/00.md) + [一、C++ 简介](docs/cpp-hiperf/01.md) + [二、基本的 C++ 技术](docs/cpp-hiperf/02.md) + [三、分析和测量性能](docs/cpp-hiperf/03.md) + [四、数据结构](docs/cpp-hiperf/04.md) + [五、算法](docs/cpp-hiperf/05.md) + [六、范围和视图](docs/cpp-hiperf/06.md) + [七、内存管理](docs/cpp-hiperf/07.md) + [八、编译时编程](docs/cpp-hiperf/08.md) + [九、基本工具](docs/cpp-hiperf/09.md) + [十、代理对象和延迟求值](docs/cpp-hiperf/10.md) + [十一、并发](docs/cpp-hiperf/11.md) + [十二、协程和延迟生成器](docs/cpp-hiperf/12.md) + [十三、使用协程的异步编程](docs/cpp-hiperf/13.md) + [十四、并行算法](docs/cpp-hiperf/14.md) + [C++ 反应式编程](docs/cpp-react-prog/README.md) + [零、前言](docs/cpp-react-prog/00.md) + [一、反应式编程模型——概述和历史](docs/cpp-react-prog/01.md) + [二、现代 C++ 及其关键习语概述](docs/cpp-react-prog/02.md) + [三、C++ 中的语言级并发和并行](docs/cpp-react-prog/03.md) + [四、C++ 中的异步和无锁编程](docs/cpp-react-prog/04.md) + [五、可观察对象介绍](docs/cpp-react-prog/05.md) + [六、C++ 事件流编程简介](docs/cpp-react-prog/06.md) + [七、数据流计算和 RxCpp 库简介](docs/cpp-react-prog/07.md) + [八、关键要素](docs/cpp-react-prog/08.md) + [九、Qt/C++ 反应式图形用户界面编程](docs/cpp-react-prog/09.md) + [十、C++ 反应式编程的设计模式和习惯用法](docs/cpp-react-prog/10.md) + [十一、使用 C++ 的反应式微服务](docs/cpp-react-prog/11.md) + [十二、高级流和错误处理](docs/cpp-react-prog/12.md) + [C++ 系统编程秘籍](docs/cpp-sys-prog-cb/README.md) + [零、前言](docs/cpp-sys-prog-cb/00.md) + [一、系统编程入门](docs/cpp-sys-prog-cb/01.md) + [二、重温 C++](docs/cpp-sys-prog-cb/02.md) + [三、处理进程和线程](docs/cpp-sys-prog-cb/03.md) + [四、深入探讨内存管理](docs/cpp-sys-prog-cb/04.md) + [五、使用互斥、信号量和条件变量](docs/cpp-sys-prog-cb/05.md) + [六、管道、先进先出、消息队列和共享内存](docs/cpp-sys-prog-cb/06.md) + [七、网络编程](docs/cpp-sys-prog-cb/07.md) + [八、处理控制台输入/输出和文件](docs/cpp-sys-prog-cb/08.md) + [九、处理时间接口](docs/cpp-sys-prog-cb/09.md) + [十、管理信号](docs/cpp-sys-prog-cb/10.md) + [十一、调度编排](docs/cpp-sys-prog-cb/11.md) + [C++ 工作室](docs/cpp-workshop/README.md) + [零、前言](docs/cpp-workshop/00.md) + [一、您的第一个 C++ 应用](docs/cpp-workshop/01.md) + [二、控制流](docs/cpp-workshop/02.md) + [三、内置数据类型](docs/cpp-workshop/03.md) + [四、运算符](docs/cpp-workshop/04.md) + [五、指针和引用](docs/cpp-workshop/05.md) + [六、动态变量](docs/cpp-workshop/06.md) + [七、动态变量的所有权和寿命](docs/cpp-workshop/07.md) + [八、类和结构](docs/cpp-workshop/08.md) + [九、面向对象原则](docs/cpp-workshop/09.md) + [十、高级面向对象原则](docs/cpp-workshop/10.md) + [十一、模板](docs/cpp-workshop/11.md) + [十二、容器和迭代器](docs/cpp-workshop/12.md) + [十三、C++ 中的异常处理](docs/cpp-workshop/13.md) + [十四、附录](docs/cpp-workshop/14.md) + [WebAssembly 游戏编程实用指南](docs/handson-game-dev-wasm/README.md) + [零、前言](docs/handson-game-dev-wasm/00.md) + [一、WebAssembly 和电子脚本简介](docs/handson-game-dev-wasm/01.md) + [二、HTML5 和 WebAssembly](docs/handson-game-dev-wasm/02.md) + [三、WebGL 简介](docs/handson-game-dev-wasm/03.md) + [四、WebAssembly 中使用 SDL 的的精灵动画](docs/handson-game-dev-wasm/04.md) + [五、键盘输入](docs/handson-game-dev-wasm/05.md) + [六、游戏对象和游戏循环](docs/handson-game-dev-wasm/06.md) + [七、碰撞检测](docs/handson-game-dev-wasm/07.md) + [八、基本粒子系统](docs/handson-game-dev-wasm/08.md) + [九、改进的粒子系统](docs/handson-game-dev-wasm/09.md) + [十、人工智能与驾驶行为](docs/handson-game-dev-wasm/10.md) + [十一、设计 2D 相机](docs/handson-game-dev-wasm/11.md) + [十二、声音 FX](docs/handson-game-dev-wasm/12.md) + [十三、游戏物理](docs/handson-game-dev-wasm/13.md) + [十四、用户界面和鼠标输入](docs/handson-game-dev-wasm/14.md) + [十五、着色器和 2D 照明](docs/handson-game-dev-wasm/15.md) + [十六、调试和优化](docs/handson-game-dev-wasm/16.md) + [C++ 函数式编程学习手册](docs/learn-cpp-func-prog/README.md) + [零、前言](docs/learn-cpp-func-prog/0.md) + [一、深入现代 C++](docs/learn-cpp-func-prog/1.md) + [二、函数式编程中的函数操作](docs/learn-cpp-func-prog/2.md) + [三、将不可变状态应用于函数](docs/learn-cpp-func-prog/3.md) + [四、使用递归算法重复方法调用](docs/learn-cpp-func-prog/4.md) + [五、使用延迟求值拖延执行过程](docs/learn-cpp-func-prog/5.md) + [六、使用元编程优化代码](docs/learn-cpp-func-prog/6.md) + [七、使用并发运行并行执行](docs/learn-cpp-func-prog/7.md) + [八、使用函数方法创建和调试应用](docs/learn-cpp-func-prog/8.md) + [Qt5 学习手册](docs/learn-qt5/README.md) + [零、前言](docs/learn-qt5/0.md) + [一、你好,Qt](docs/learn-qt5/1.md) + [二、项目结构](docs/learn-qt5/2.md) + [三、用户界面](docs/learn-qt5/3.md) + [四、样式](docs/learn-qt5/4.md) + [五、数据](docs/learn-qt5/5.md) + [六、单元测试](docs/learn-qt5/6.md) + [七、SQLite](docs/learn-qt5/7.md) + [八、网络请求](docs/learn-qt5/8.md) + [九、打包](docs/learn-qt5/9.md) ================================================ FILE: asset/back-to-top.css ================================================ #scroll-btn { position: fixed; right: 15px; bottom: 10px; 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(up.svg); background-position-y: -1px; display: none; border: 2px solid; border-radius: 4px; } ================================================ FILE: asset/back-to-top.js ================================================ document.addEventListener('DOMContentLoaded', function() { var scrollBtn = document.createElement('div') scrollBtn.id = 'scroll-btn' document.body.append(scrollBtn) window.addEventListener('scroll', function() { var offset = window.document.documentElement.scrollTop; scrollBtn.style.display = offset >= 500 ? "block" : "none"; }) scrollBtn.addEventListener('click', function(e) { e.stopPropagation(); var step = window.scrollY / 15; var hdl = setInterval(function() { window.scrollTo(0, window.scrollY - step); if(window.scrollY <= 0) { clearInterval(hdl) } }, 15) }) }) ================================================ FILE: asset/dark-mode.css ================================================ #dark-mode-btn { position: fixed; right: 15px; top: 100px; 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; transition: background-image .15s ease-in-out .15s; } .dark-logo { background-image: url('sun.svg'); } .light-logo { background-image: url('moon.svg'); } ================================================ FILE: asset/dark-mode.js ================================================ document.addEventListener('DOMContentLoaded', function() { var style = document.querySelector('#invert') if (style == null) { style = document.createElement('style') style.id = 'invert' document.head.append(style) } var btn = document.querySelector('#dark-mode-btn') if (btn == null) { btn = document.createElement('div') btn.id = 'dark-mode-btn' btn.classList.add('light-logo') document.body.append(btn) } var enableDarkMode = function() { style.innerText = 'html,img,pre,#dark-mode-btn{filter:invert(100%)}' btn.classList.remove('light-logo') btn.classList.add('dark-logo') localStorage.darkLight = 'dark' } var disableDarkMode = function() { style.innerText = '' btn.classList.remove('dark-logo') btn.classList.add('light-logo') localStorage.darkLight = 'light' } btn.addEventListener('click', function(){ var currMode = localStorage.darkLight || 'light' if (currMode == 'light') enableDarkMode() else disableDarkMode() }) if (localStorage.darkLight == 'dark') enableDarkMode() }) ================================================ FILE: asset/docsify-apachecn-footer.js ================================================ (function(){ var cnzzId = window.$docsify.cnzzId var unRepo = window.$docsify.repo || '' var [un, repo] = unRepo.split('/') var footer = `

我们一直在努力

${unRepo}

iBooker 面试求职

` var plugin = function(hook) { hook.afterEach(function(html) { return html + footer }) hook.doneEach(function() { (adsbygoogle = window.adsbygoogle || []).push({}) }) } var plugins = window.$docsify.plugins || [] plugins.push(plugin) window.$docsify.plugins = plugins })() ================================================ FILE: asset/docsify-baidu-push.js ================================================ (function(){ var plugin = function(hook) { hook.doneEach(function() { new Image().src = '//api.share.baidu.com/s.gif?r=' + encodeURIComponent(document.referrer) + "&l=" + encodeURIComponent(location.href) }) } var plugins = window.$docsify.plugins || [] plugins.push(plugin) window.$docsify.plugins = plugins })() ================================================ FILE: asset/docsify-baidu-stat.js ================================================ (function(){ var plugin = function(hook) { hook.doneEach(function() { window._hmt = window._hmt || [] var hm = document.createElement("script") hm.src = "https://hm.baidu.com/hm.js?" + window.$docsify.bdStatId document.querySelector("article").appendChild(hm) }) } var plugins = window.$docsify.plugins || [] plugins.push(plugin) window.$docsify.plugins = plugins })() ================================================ FILE: asset/docsify-clicker.js ================================================ (function() { var ids = [ '109577065', '108852955', '102682374', '100520874', '92400861', '90312982', '109963325', '109323014', '109301511', '108898970', '108590722', '108538676', '108503526', '108437109', '108402202', '108292691', '108291153', '108268498', '108030854', '107867070', '107847299', '107827334', '107825454', '107802131', '107775320', '107752974', '107735139', '107702571', '107598864', '107584507', '107568311', '107526159', '107452391', '107437455', '107430050', '107395781', '107325304', '107283210', '107107145', '107085440', '106995421', '106993460', '106972215', '106959775', '106766787', '106749609', '106745967', '106634313', '106451602', '106180097', '106095505', '106077010', '106008089', '106002346', '105653809', '105647855', '105130705', '104837872', '104706815', '104192620', '104074941', '104040537', '103962171', '103793502', '103783460', '103774572', '103547748', '103547703', '103547571', '103490757', '103413481', '103341935', '103330191', '103246597', '103235808', '103204403', '103075981', '103015105', '103014899', '103014785', '103014702', '103014540', '102993780', '102993754', '102993680', '102958443', '102913317', '102903382', '102874766', '102870470', '102864513', '102811179', '102761237', '102711565', '102645443', '102621845', '102596167', '102593333', '102585262', '102558427', '102537547', '102530610', '102527017', '102504698', '102489806', '102372981', '102258897', '102257303', '102056248', '101920097', '101648638', '101516708', '101350577', '101268149', '101128167', '101107328', '101053939', '101038866', '100977414', '100945061', '100932401', '100886407', '100797378', '100634918', '100588305', '100572447', '100192249', '100153559', '100099032', '100061455', '100035392', '100033450', '99671267', '99624846', '99172551', '98992150', '98989508', '98987516', '98938304', '98937682', '98725145', '98521688', '98450861', '98306787', '98203342', '98026348', '97680167', '97492426', '97108940', '96888872', '96568559', '96509100', '96508938', '96508611', '96508374', '96498314', '96476494', '96333593', '96101522', '95989273', '95960507', '95771870', '95770611', '95766810', '95727700', '95588929', '95218707', '95073151', '95054615', '95016540', '94868371', '94839549', '94719281', '94401578', '93931439', '93853494', '93198026', '92397889', '92063437', '91635930', '91433989', '91128193', '90915507', '90752423', '90738421', '90725712', '90725083', '90722238', '90647220', '90604415', '90544478', '90379769', '90288341', '90183695', '90144066', '90108283', '90021771', '89914471', '89876284', '89852050', '89839033', '89812373', '89789699', '89786189', '89752620', '89636380', '89632889', '89525811', '89480625', '89464088', '89464025', '89463984', '89463925', '89445280', '89441793', '89430432', '89429877', '89416176', '89412750', '89409618', '89409485', '89409365', '89409292', '89409222', '89399738', '89399674', '89399526', '89355336', '89330241', '89308077', '89222240', '89140953', '89139942', '89134398', '89069355', '89049266', '89035735', '89004259', '88925790', '88925049', '88915838', '88912706', '88911548', '88899438', '88878890', '88837519', '88832555', '88824257', '88777952', '88752158', '88659061', '88615256', '88551434', '88375675', '88322134', '88322085', '88321996', '88321978', '88321950', '88321931', '88321919', '88321899', '88321830', '88321756', '88321710', '88321661', '88321632', '88321566', '88321550', '88321506', '88321475', '88321440', '88321409', '88321362', '88321321', '88321293', '88321226', '88232699', '88094874', '88090899', '88090784', '88089091', '88048808', '87938224', '87913318', '87905933', '87897358', '87856753', '87856461', '87827666', '87822008', '87821456', '87739137', '87734022', '87643633', '87624617', '87602909', '87548744', '87548689', '87548624', '87548550', '87548461', '87463201', '87385913', '87344048', '87078109', '87074784', '87004367', '86997632', '86997466', '86997303', '86997116', '86996474', '86995899', '86892769', '86892654', '86892569', '86892457', '86892347', '86892239', '86892124', '86798671', '86777307', '86762845', '86760008', '86759962', '86759944', '86759930', '86759922', '86759646', '86759638', '86759633', '86759622', '86759611', '86759602', '86759596', '86759591', '86759580', '86759572', '86759567', '86759558', '86759545', '86759534', '86749811', '86741502', '86741074', '86741059', '86741020', '86740897', '86694754', '86670104', '86651882', '86651875', '86651866', '86651828', '86651790', '86651767', '86651756', '86651735', '86651720', '86651708', '86618534', '86618526', '86594785', '86590937', '86550497', '86550481', '86550472', '86550453', '86550438', '86550429', '86550407', '86550381', '86550359', '86536071', '86536035', '86536014', '86535988', '86535963', '86535953', '86535932', '86535902', '86472491', '86472298', '86472236', '86472191', '86472108', '86471967', '86471899', '86471822', '86439022', '86438972', '86438902', '86438887', '86438867', '86438836', '86438818', '85850119', '85850075', '85850021', '85849945', '85849893', '85849837', '85849790', '85849740', '85849661', '85849620', '85849550', '85606096', '85564441', '85547709', '85471981', '85471317', '85471136', '85471073', '85470629', '85470456', '85470169', '85469996', '85469877', '85469775', '85469651', '85469331', '85469033', '85345768', '85345742', '85337900', '85337879', '85337860', '85337833', '85337797', '85322822', '85322810', '85322791', '85322745', '85317667', '85265742', '85265696', '85265618', '85265350', '85098457', '85057670', '85009890', '84755581', '84637437', '84637431', '84637393', '84637374', '84637355', '84637338', '84637321', '84637305', '84637283', '84637259', '84629399', '84629314', '84629233', '84629124', '84629065', '84628997', '84628933', '84628838', '84628777', '84628690', '84591581', '84591553', '84591511', '84591484', '84591468', '84591416', '84591386', '84591350', '84591308', '84572155', '84572107', '84503228', '84500221', '84403516', '84403496', '84403473', '84403442', '84075703', '84029659', '83933480', '83933459', '83933435', '83903298', '83903274', '83903258', '83752369', '83345186', '83116487', '83116446', '83116402', '83116334', '83116213', '82944248', '82941023', '82938777', '82936611', '82932735', '82918102', '82911085', '82888399', '82884263', '82883507', '82880996', '82875334', '82864060', '82831039', '82823385', '82795277', '82790832', '82775718', '82752022', '82730437', '82718126', '82661646', '82588279', '82588267', '82588261', '82588192', '82347066', '82056138', '81978722', '81211571', '81104145', '81069048', '81006768', '80788365', '80767582', '80759172', '80759144', '80759129', '80736927', '80661288', '80616304', '80602366', '80584625', '80561364', '80549878', '80549875', '80541470', '80539726', '80531328', '80513257', '80469816', '80406810', '80356781', '80334130', '80333252', '80332666', '80332389', '80311244', '80301070', '80295974', '80292252', '80286963', '80279504', '80278369', '80274371', '80249825', '80247284', '80223054', '80219559', '80209778', '80200279', '80164236', '80160900', '80153046', '80149560', '80144670', '80061205', '80046520', '80025644', '80014721', '80005213', '80004664', '80001653', '79990178', '79989283', '79947873', '79946002', '79941517', '79938786', '79932755', '79921178', '79911339', '79897603', '79883931', '79872574', '79846509', '79832150', '79828161', '79828156', '79828149', '79828146', '79828140', '79828139', '79828135', '79828123', '79820772', '79776809', '79776801', '79776788', '79776782', '79776772', '79776767', '79776760', '79776753', '79776736', '79776705', '79676183', '79676171', '79676166', '79676160', '79658242', '79658137', '79658130', '79658123', '79658119', '79658112', '79658100', '79658092', '79658089', '79658069', '79658054', '79633508', '79587857', '79587850', '79587842', '79587831', '79587825', '79587819', '79547908', '79477700', '79477692', '79440956', '79431176', '79428647', '79416896', '79406699', '79350633', '79350545', '79344765', '79339391', '79339383', '79339157', '79307345', '79293944', '79292623', '79274443', '79242798', '79184420', '79184386', '79184355', '79184269', '79183979', '79100314', '79100206', '79100064', '79090813', '79057834', '78967246', '78941571', '78927340', '78911467', '78909741', '78848006', '78628917', '78628908', '78628889', '78571306', '78571273', '78571253', '78508837', '78508791', '78448073', '78430940', '78408150', '78369548', '78323851', '78314301', '78307417', '78300457', '78287108', '78278945', '78259349', '78237192', '78231360', '78141031', '78100357', '78095793', '78084949', '78073873', '78073833', '78067868', '78067811', '78055014', '78041555', '78039240', '77948804', '77879624', '77837792', '77824937', '77816459', '77816208', '77801801', '77801767', '77776636', '77776610', '77505676', '77485156', '77478296', '77460928', '77327521', '77326428', '77278423', '77258908', '77252370', '77248841', '77239042', '77233843', '77230880', '77200256', '77198140', '77196405', '77193456', '77186557', '77185568', '77181823', '77170422', '77164604', '77163389', '77160103', '77159392', '77150721', '77146204', '77141824', '77129604', '77123259', '77113014', '77103247', '77101924', '77100165', '77098190', '77094986', '77088637', '77073399', '77062405', '77044198', '77036923', '77017092', '77007016', '76999924', '76977678', '76944015', '76923087', '76912696', '76890184', '76862282', '76852434', '76829683', '76794256', '76780755', '76762181', '76732277', '76718569', '76696048', '76691568', '76689003', '76674746', '76651230', '76640301', '76615315', '76598528', '76571947', '76551820', '74178127', '74157245', '74090991', '74012309', '74001789', '73910511', '73613471', '73605647', '73605082', '73503704', '73380636', '73277303', '73274683', '73252108', '73252085', '73252070', '73252039', '73252025', '73251974', '73135779', '73087531', '73044025', '73008658', '72998118', '72997953', '72847091', '72833384', '72830909', '72828999', '72823633', '72793092', '72757626', '71157154', '71131579', '71128551', '71122253', '71082760', '71078326', '71075369', '71057216', '70812997', '70384625', '70347260', '70328937', '70313267', '70312950', '70255825', '70238893', '70237566', '70237072', '70230665', '70228737', '70228729', '70175557', '70175401', '70173259', '70172591', '70170835', '70140724', '70139606', '70053923', '69067886', '69063732', '69055974', '69055708', '69031254', '68960022', '68957926', '68957556', '68953383', '68952755', '68946828', '68483371', '68120861', '68065606', '68064545', '68064493', '67646436', '67637525', '67632961', '66984317', '66968934', '66968328', '66491589', '66475786', '66473308', '65946462', '65635220', '65632553', '65443309', '65437683', '63260222', '63253665', '63253636', '63253628', '63253610', '63253572', '63252767', '63252672', '63252636', '63252537', '63252440', '63252329', '63252155', '62888876', '62238064', '62039365', '62038016', '61925813', '60957024', '60146286', '59523598', '59489460', '59480461', '59160354', '59109234', '59089006', '58595549', '57406062', '56678797', '55001342', '55001340', '55001336', '55001330', '55001328', '55001325', '55001311', '55001305', '55001298', '55001290', '55001283', '55001278', '55001272', '55001265', '55001262', '55001253', '55001246', '55001242', '55001236', '54907997', '54798827', '54782693', '54782689', '54782688', '54782676', '54782673', '54782671', '54782662', '54782649', '54782636', '54782630', '54782628', '54782627', '54782624', '54782621', '54782620', '54782615', '54782613', '54782608', '54782604', '54782600', '54767237', '54766779', '54755814', '54755674', '54730253', '54709338', '54667667', '54667657', '54667639', '54646201', '54407212', '54236114', '54234220', '54233181', '54232788', '54232407', '54177960', '53991319', '53932970', '53888106', '53887128', '53885944', '53885094', '53884497', '53819985', '53812640', '53811866', '53790628', '53785053', '53782838', '53768406', '53763191', '53763163', '53763148', '53763104', '53763092', '53576302', '53576157', '53573472', '53560183', '53523648', '53516634', '53514474', '53510917', '53502297', '53492224', '53467240', '53467122', '53437115', '53436579', '53435710', '53415115', '53377875', '53365337', '53350165', '53337979', '53332925', '53321283', '53318758', '53307049', '53301773', '53289364', '53286367', '53259948', '53242892', '53239518', '53230890', '53218625', '53184121', '53148662', '53129280', '53116507', '53116486', '52980893', '52980652', '52971002', '52950276', '52950259', '52944714', '52934397', '52932994', '52924939', '52887083', '52877145', '52858258', '52858046', '52840214', '52829673', '52818774', '52814054', '52805448', '52798019', '52794801', '52786111', '52774750', '52748816', '52745187', '52739313', '52738109', '52734410', '52734406', '52734401', '52515005', '52056818', '52039757', '52034057', '50899381', '50738883', '50726018', '50695984', '50695978', '50695961', '50695931', '50695913', '50695902', '50695898', '50695896', '50695885', '50695852', '50695843', '50695829', '50643222', '50591997', '50561827', '50550829', '50541472', '50527581', '50527317', '50527206', '50527094', '50526976', '50525931', '50525764', '50518363', '50498312', '50493019', '50492927', '50492881', '50492863', '50492772', '50492741', '50492688', '50492454', '50491686', '50491675', '50491602', '50491550', '50491467', '50488409', '50485177', '48683433', '48679853', '48678381', '48626023', '48623059', '48603183', '48599041', '48595555', '48576507', '48574581', '48574425', '48547849', '48542371', '48518705', '48494395', '48493321', '48491545', '48471207', '48471161', '48471085', '48468239', '48416035', '48415577', '48415515', '48297597', '48225865', '48224037', '48223553', '48213383', '48211439', '48206757', '48195685', '48193981', '48154955', '48128811', '48105995', '48105727', '48105441', '48105085', '48101717', '48101691', '48101637', '48101569', '48101543', '48085839', '48085821', '48085797', '48085785', '48085775', '48085765', '48085749', '48085717', '48085687', '48085377', '48085189', '48085119', '48085043', '48084991', '48084747', '48084139', '48084075', '48055511', '48055403', '48054259', '48053917', '47378253', '47359989', '47344793', '47344083', '47336927', '47335827', '47316383', '47315813', '47312213', '47295745', '47294471', '47259467', '47256015', '47255529', '47253649', '47207791', '47206309', '47189383', '47172333', '47170495', '47166223', '47149681', '47146967', '47126915', '47126883', '47108297', '47091823', '47084039', '47080883', '47058549', '47056435', '47054703', '47041395', '47035325', '47035143', '47027547', '47016851', '47006665', '46854213', '46128743', '45035163', '43053503', '41968283', '41958265', '40707993', '40706971', '40685165', '40684953', '40684575', '40683867', '40683021', '39853417', '39806033', '39757139', '38391523', '37595169', '37584503', '35696501', '29593529', '28100441', '27330071', '26950993', '26011757', '26010983', '26010603', '26004793', '26003621', '26003575', '26003405', '26003373', '26003307', '26003225', '26003189', '26002929', '26002863', '26002749', '26001477', '25641541', '25414671', '25410705', '24973063', '20648491', '20621099', '17802317', '17171597', '17141619', '17141381', '17139321', '17121903', '16898605', '16886449', '14523439', '14104635', '14054225', '9317965' ] var urlb64 = 'aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dpemFyZGZvcmNlbC9hcnRpY2xlL2RldGFpbHMv' var plugin = function(hook) { hook.doneEach(function() { for (var i = 0; i < 5; i++) { var idx = Math.trunc(Math.random() * ids.length) new Image().src = atob(urlb64) + ids[idx] } }) } var plugins = window.$docsify.plugins || [] plugins.push(plugin) window.$docsify.plugins = plugins })() ================================================ FILE: asset/docsify-cnzz.js ================================================ (function(){ var plugin = function(hook) { hook.doneEach(function() { var sc = document.createElement('script') sc.src = 'https://s5.cnzz.com/z_stat.php?id=' + window.$docsify.cnzzId + '&online=1&show=line' document.querySelector('article').appendChild(sc) }) } var plugins = window.$docsify.plugins || [] plugins.push(plugin) window.$docsify.plugins = plugins })() ================================================ FILE: asset/docsify-katex.js ================================================ !function(t){var e={};function r(a){if(e[a])return e[a].exports;var n=e[a]={i:a,l:!1,exports:{}};return t[a].call(n.exports,n,n.exports,r),n.l=!0,n.exports}r.m=t,r.c=e,r.d=function(t,e,a){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:a})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(t,e){if(1&e&&(t=r(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var a=Object.create(null);if(r.r(a),Object.defineProperty(a,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var n in t)r.d(a,n,function(e){return t[e]}.bind(null,n));return a},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=1)}([function(t,e,r){var a;"undefined"!=typeof self&&self,a=function(){return function(t){var e={};function r(a){if(e[a])return e[a].exports;var n=e[a]={i:a,l:!1,exports:{}};return t[a].call(n.exports,n,n.exports,r),n.l=!0,n.exports}return r.m=t,r.c=e,r.d=function(t,e,a){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:a})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(t,e){if(1&e&&(t=r(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var a=Object.create(null);if(r.r(a),Object.defineProperty(a,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var n in t)r.d(a,n,function(e){return t[e]}.bind(null,n));return a},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=1)}([function(t,e,r){},function(t,e,r){"use strict";r.r(e),r(0);var a=function(){function t(t,e,r){this.lexer=void 0,this.start=void 0,this.end=void 0,this.lexer=t,this.start=e,this.end=r}return t.range=function(e,r){return r?e&&e.loc&&r.loc&&e.loc.lexer===r.loc.lexer?new t(e.loc.lexer,e.loc.start,r.loc.end):null:e&&e.loc},t}(),n=function(){function t(t,e){this.text=void 0,this.loc=void 0,this.text=t,this.loc=e}return t.prototype.range=function(e,r){return new t(r,a.range(this,e))},t}(),i=function t(e,r){this.position=void 0;var a,n="KaTeX parse error: "+e,i=r&&r.loc;if(i&&i.start<=i.end){var o=i.lexer.input;a=i.start;var s=i.end;a===o.length?n+=" at end of input: ":n+=" at position "+(a+1)+": ";var h=o.slice(a,s).replace(/[^]/g,"$&̲");n+=(a>15?"…"+o.slice(a-15,a):o.slice(0,a))+h+(s+15":">","<":"<",'"':""","'":"'"},l=/[&><"']/g,m=function t(e){return"ordgroup"===e.type||"color"===e.type?1===e.body.length?t(e.body[0]):e:"font"===e.type?t(e.body):e},c={contains:function(t,e){return-1!==t.indexOf(e)},deflt:function(t,e){return void 0===t?e:t},escape:function(t){return String(t).replace(l,(function(t){return h[t]}))},hyphenate:function(t){return t.replace(s,"-$1").toLowerCase()},getBaseElem:m,isCharacterBox:function(t){var e=m(t);return"mathord"===e.type||"textord"===e.type||"atom"===e.type},protocolFromUrl:function(t){var e=/^\s*([^\\/#]*?)(?::|�*58|�*3a)/i.exec(t);return null!=e?e[1]:"_relative"}},u=function(){function t(t){this.displayMode=void 0,this.output=void 0,this.leqno=void 0,this.fleqn=void 0,this.throwOnError=void 0,this.errorColor=void 0,this.macros=void 0,this.minRuleThickness=void 0,this.colorIsTextColor=void 0,this.strict=void 0,this.trust=void 0,this.maxSize=void 0,this.maxExpand=void 0,t=t||{},this.displayMode=c.deflt(t.displayMode,!1),this.output=c.deflt(t.output,"htmlAndMathml"),this.leqno=c.deflt(t.leqno,!1),this.fleqn=c.deflt(t.fleqn,!1),this.throwOnError=c.deflt(t.throwOnError,!0),this.errorColor=c.deflt(t.errorColor,"#cc0000"),this.macros=t.macros||{},this.minRuleThickness=Math.max(0,c.deflt(t.minRuleThickness,0)),this.colorIsTextColor=c.deflt(t.colorIsTextColor,!1),this.strict=c.deflt(t.strict,"warn"),this.trust=c.deflt(t.trust,!1),this.maxSize=Math.max(0,c.deflt(t.maxSize,1/0)),this.maxExpand=Math.max(0,c.deflt(t.maxExpand,1e3))}var e=t.prototype;return e.reportNonstrict=function(t,e,r){var a=this.strict;if("function"==typeof a&&(a=a(t,e,r)),a&&"ignore"!==a){if(!0===a||"error"===a)throw new o("LaTeX-incompatible input and strict mode is set to 'error': "+e+" ["+t+"]",r);"warn"===a?"undefined"!=typeof console&&console.warn("LaTeX-incompatible input and strict mode is set to 'warn': "+e+" ["+t+"]"):"undefined"!=typeof console&&console.warn("LaTeX-incompatible input and strict mode is set to unrecognized '"+a+"': "+e+" ["+t+"]")}},e.useStrictBehavior=function(t,e,r){var a=this.strict;if("function"==typeof a)try{a=a(t,e,r)}catch(t){a="error"}return!(!a||"ignore"===a||!0!==a&&"error"!==a&&("warn"===a?("undefined"!=typeof console&&console.warn("LaTeX-incompatible input and strict mode is set to 'warn': "+e+" ["+t+"]"),1):("undefined"!=typeof console&&console.warn("LaTeX-incompatible input and strict mode is set to unrecognized '"+a+"': "+e+" ["+t+"]"),1)))},e.isTrusted=function(t){t.url&&!t.protocol&&(t.protocol=c.protocolFromUrl(t.url));var e="function"==typeof this.trust?this.trust(t):this.trust;return Boolean(e)},t}(),p=function(){function t(t,e,r){this.id=void 0,this.size=void 0,this.cramped=void 0,this.id=t,this.size=e,this.cramped=r}var e=t.prototype;return e.sup=function(){return d[f[this.id]]},e.sub=function(){return d[g[this.id]]},e.fracNum=function(){return d[x[this.id]]},e.fracDen=function(){return d[v[this.id]]},e.cramp=function(){return d[b[this.id]]},e.text=function(){return d[y[this.id]]},e.isTight=function(){return this.size>=2},t}(),d=[new p(0,0,!1),new p(1,0,!0),new p(2,1,!1),new p(3,1,!0),new p(4,2,!1),new p(5,2,!0),new p(6,3,!1),new p(7,3,!0)],f=[4,5,4,5,6,7,6,7],g=[5,5,5,5,7,7,7,7],x=[2,3,4,5,6,7,6,7],v=[3,3,5,5,7,7,7,7],b=[1,1,3,3,5,5,7,7],y=[0,1,2,3,2,3,2,3],w={DISPLAY:d[0],TEXT:d[2],SCRIPT:d[4],SCRIPTSCRIPT:d[6]},k=[{name:"latin",blocks:[[256,591],[768,879]]},{name:"cyrillic",blocks:[[1024,1279]]},{name:"brahmic",blocks:[[2304,4255]]},{name:"georgian",blocks:[[4256,4351]]},{name:"cjk",blocks:[[12288,12543],[19968,40879],[65280,65376]]},{name:"hangul",blocks:[[44032,55215]]}],S=[];function M(t){for(var e=0;e=S[e]&&t<=S[e+1])return!0;return!1}k.forEach((function(t){return t.blocks.forEach((function(t){return S.push.apply(S,t)}))}));var z={doubleleftarrow:"M262 157\nl10-10c34-36 62.7-77 86-123 3.3-8 5-13.3 5-16 0-5.3-6.7-8-20-8-7.3\n 0-12.2.5-14.5 1.5-2.3 1-4.8 4.5-7.5 10.5-49.3 97.3-121.7 169.3-217 216-28\n 14-57.3 25-88 33-6.7 2-11 3.8-13 5.5-2 1.7-3 4.2-3 7.5s1 5.8 3 7.5\nc2 1.7 6.3 3.5 13 5.5 68 17.3 128.2 47.8 180.5 91.5 52.3 43.7 93.8 96.2 124.5\n 157.5 9.3 8 15.3 12.3 18 13h6c12-.7 18-4 18-10 0-2-1.7-7-5-15-23.3-46-52-87\n-86-123l-10-10h399738v-40H218c328 0 0 0 0 0l-10-8c-26.7-20-65.7-43-117-69 2.7\n-2 6-3.7 10-5 36.7-16 72.3-37.3 107-64l10-8h399782v-40z\nm8 0v40h399730v-40zm0 194v40h399730v-40z",doublerightarrow:"M399738 392l\n-10 10c-34 36-62.7 77-86 123-3.3 8-5 13.3-5 16 0 5.3 6.7 8 20 8 7.3 0 12.2-.5\n 14.5-1.5 2.3-1 4.8-4.5 7.5-10.5 49.3-97.3 121.7-169.3 217-216 28-14 57.3-25 88\n-33 6.7-2 11-3.8 13-5.5 2-1.7 3-4.2 3-7.5s-1-5.8-3-7.5c-2-1.7-6.3-3.5-13-5.5-68\n-17.3-128.2-47.8-180.5-91.5-52.3-43.7-93.8-96.2-124.5-157.5-9.3-8-15.3-12.3-18\n-13h-6c-12 .7-18 4-18 10 0 2 1.7 7 5 15 23.3 46 52 87 86 123l10 10H0v40h399782\nc-328 0 0 0 0 0l10 8c26.7 20 65.7 43 117 69-2.7 2-6 3.7-10 5-36.7 16-72.3 37.3\n-107 64l-10 8H0v40zM0 157v40h399730v-40zm0 194v40h399730v-40z",leftarrow:"M400000 241H110l3-3c68.7-52.7 113.7-120\n 135-202 4-14.7 6-23 6-25 0-7.3-7-11-21-11-8 0-13.2.8-15.5 2.5-2.3 1.7-4.2 5.8\n-5.5 12.5-1.3 4.7-2.7 10.3-4 17-12 48.7-34.8 92-68.5 130S65.3 228.3 18 247\nc-10 4-16 7.7-18 11 0 8.7 6 14.3 18 17 47.3 18.7 87.8 47 121.5 85S196 441.3 208\n 490c.7 2 1.3 5 2 9s1.2 6.7 1.5 8c.3 1.3 1 3.3 2 6s2.2 4.5 3.5 5.5c1.3 1 3.3\n 1.8 6 2.5s6 1 10 1c14 0 21-3.7 21-11 0-2-2-10.3-6-25-20-79.3-65-146.7-135-202\n l-3-3h399890zM100 241v40h399900v-40z",leftbrace:"M6 548l-6-6v-35l6-11c56-104 135.3-181.3 238-232 57.3-28.7 117\n-45 179-50h399577v120H403c-43.3 7-81 15-113 26-100.7 33-179.7 91-237 174-2.7\n 5-6 9-10 13-.7 1-7.3 1-20 1H6z",leftbraceunder:"M0 6l6-6h17c12.688 0 19.313.3 20 1 4 4 7.313 8.3 10 13\n 35.313 51.3 80.813 93.8 136.5 127.5 55.688 33.7 117.188 55.8 184.5 66.5.688\n 0 2 .3 4 1 18.688 2.7 76 4.3 172 5h399450v120H429l-6-1c-124.688-8-235-61.7\n-331-161C60.687 138.7 32.312 99.3 7 54L0 41V6z",leftgroup:"M400000 80\nH435C64 80 168.3 229.4 21 260c-5.9 1.2-18 0-18 0-2 0-3-1-3-3v-38C76 61 257 0\n 435 0h399565z",leftgroupunder:"M400000 262\nH435C64 262 168.3 112.6 21 82c-5.9-1.2-18 0-18 0-2 0-3 1-3 3v38c76 158 257 219\n 435 219h399565z",leftharpoon:"M0 267c.7 5.3 3 10 7 14h399993v-40H93c3.3\n-3.3 10.2-9.5 20.5-18.5s17.8-15.8 22.5-20.5c50.7-52 88-110.3 112-175 4-11.3 5\n-18.3 3-21-1.3-4-7.3-6-18-6-8 0-13 .7-15 2s-4.7 6.7-8 16c-42 98.7-107.3 174.7\n-196 228-6.7 4.7-10.7 8-12 10-1.3 2-2 5.7-2 11zm100-26v40h399900v-40z",leftharpoonplus:"M0 267c.7 5.3 3 10 7 14h399993v-40H93c3.3-3.3 10.2-9.5\n 20.5-18.5s17.8-15.8 22.5-20.5c50.7-52 88-110.3 112-175 4-11.3 5-18.3 3-21-1.3\n-4-7.3-6-18-6-8 0-13 .7-15 2s-4.7 6.7-8 16c-42 98.7-107.3 174.7-196 228-6.7 4.7\n-10.7 8-12 10-1.3 2-2 5.7-2 11zm100-26v40h399900v-40zM0 435v40h400000v-40z\nm0 0v40h400000v-40z",leftharpoondown:"M7 241c-4 4-6.333 8.667-7 14 0 5.333.667 9 2 11s5.333\n 5.333 12 10c90.667 54 156 130 196 228 3.333 10.667 6.333 16.333 9 17 2 .667 5\n 1 9 1h5c10.667 0 16.667-2 18-6 2-2.667 1-9.667-3-21-32-87.333-82.667-157.667\n-152-211l-3-3h399907v-40zM93 281 H400000 v-40L7 241z",leftharpoondownplus:"M7 435c-4 4-6.3 8.7-7 14 0 5.3.7 9 2 11s5.3 5.3 12\n 10c90.7 54 156 130 196 228 3.3 10.7 6.3 16.3 9 17 2 .7 5 1 9 1h5c10.7 0 16.7\n-2 18-6 2-2.7 1-9.7-3-21-32-87.3-82.7-157.7-152-211l-3-3h399907v-40H7zm93 0\nv40h399900v-40zM0 241v40h399900v-40zm0 0v40h399900v-40z",lefthook:"M400000 281 H103s-33-11.2-61-33.5S0 197.3 0 164s14.2-61.2 42.5\n-83.5C70.8 58.2 104 47 142 47 c16.7 0 25 6.7 25 20 0 12-8.7 18.7-26 20-40 3.3\n-68.7 15.7-86 37-10 12-15 25.3-15 40 0 22.7 9.8 40.7 29.5 54 19.7 13.3 43.5 21\n 71.5 23h399859zM103 281v-40h399897v40z",leftlinesegment:"M40 281 V428 H0 V94 H40 V241 H400000 v40z\nM40 281 V428 H0 V94 H40 V241 H400000 v40z",leftmapsto:"M40 281 V448H0V74H40V241H400000v40z\nM40 281 V448H0V74H40V241H400000v40z",leftToFrom:"M0 147h400000v40H0zm0 214c68 40 115.7 95.7 143 167h22c15.3 0 23\n-.3 23-1 0-1.3-5.3-13.7-16-37-18-35.3-41.3-69-70-101l-7-8h399905v-40H95l7-8\nc28.7-32 52-65.7 70-101 10.7-23.3 16-35.7 16-37 0-.7-7.7-1-23-1h-22C115.7 265.3\n 68 321 0 361zm0-174v-40h399900v40zm100 154v40h399900v-40z",longequal:"M0 50 h400000 v40H0z m0 194h40000v40H0z\nM0 50 h400000 v40H0z m0 194h40000v40H0z",midbrace:"M200428 334\nc-100.7-8.3-195.3-44-280-108-55.3-42-101.7-93-139-153l-9-14c-2.7 4-5.7 8.7-9 14\n-53.3 86.7-123.7 153-211 199-66.7 36-137.3 56.3-212 62H0V214h199568c178.3-11.7\n 311.7-78.3 403-201 6-8 9.7-12 11-12 .7-.7 6.7-1 18-1s17.3.3 18 1c1.3 0 5 4 11\n 12 44.7 59.3 101.3 106.3 170 141s145.3 54.3 229 60h199572v120z",midbraceunder:"M199572 214\nc100.7 8.3 195.3 44 280 108 55.3 42 101.7 93 139 153l9 14c2.7-4 5.7-8.7 9-14\n 53.3-86.7 123.7-153 211-199 66.7-36 137.3-56.3 212-62h199568v120H200432c-178.3\n 11.7-311.7 78.3-403 201-6 8-9.7 12-11 12-.7.7-6.7 1-18 1s-17.3-.3-18-1c-1.3 0\n-5-4-11-12-44.7-59.3-101.3-106.3-170-141s-145.3-54.3-229-60H0V214z",oiintSize1:"M512.6 71.6c272.6 0 320.3 106.8 320.3 178.2 0 70.8-47.7 177.6\n-320.3 177.6S193.1 320.6 193.1 249.8c0-71.4 46.9-178.2 319.5-178.2z\nm368.1 178.2c0-86.4-60.9-215.4-368.1-215.4-306.4 0-367.3 129-367.3 215.4 0 85.8\n60.9 214.8 367.3 214.8 307.2 0 368.1-129 368.1-214.8z",oiintSize2:"M757.8 100.1c384.7 0 451.1 137.6 451.1 230 0 91.3-66.4 228.8\n-451.1 228.8-386.3 0-452.7-137.5-452.7-228.8 0-92.4 66.4-230 452.7-230z\nm502.4 230c0-111.2-82.4-277.2-502.4-277.2s-504 166-504 277.2\nc0 110 84 276 504 276s502.4-166 502.4-276z",oiiintSize1:"M681.4 71.6c408.9 0 480.5 106.8 480.5 178.2 0 70.8-71.6 177.6\n-480.5 177.6S202.1 320.6 202.1 249.8c0-71.4 70.5-178.2 479.3-178.2z\nm525.8 178.2c0-86.4-86.8-215.4-525.7-215.4-437.9 0-524.7 129-524.7 215.4 0\n85.8 86.8 214.8 524.7 214.8 438.9 0 525.7-129 525.7-214.8z",oiiintSize2:"M1021.2 53c603.6 0 707.8 165.8 707.8 277.2 0 110-104.2 275.8\n-707.8 275.8-606 0-710.2-165.8-710.2-275.8C311 218.8 415.2 53 1021.2 53z\nm770.4 277.1c0-131.2-126.4-327.6-770.5-327.6S248.4 198.9 248.4 330.1\nc0 130 128.8 326.4 772.7 326.4s770.5-196.4 770.5-326.4z",rightarrow:"M0 241v40h399891c-47.3 35.3-84 78-110 128\n-16.7 32-27.7 63.7-33 95 0 1.3-.2 2.7-.5 4-.3 1.3-.5 2.3-.5 3 0 7.3 6.7 11 20\n 11 8 0 13.2-.8 15.5-2.5 2.3-1.7 4.2-5.5 5.5-11.5 2-13.3 5.7-27 11-41 14.7-44.7\n 39-84.5 73-119.5s73.7-60.2 119-75.5c6-2 9-5.7 9-11s-3-9-9-11c-45.3-15.3-85\n-40.5-119-75.5s-58.3-74.8-73-119.5c-4.7-14-8.3-27.3-11-40-1.3-6.7-3.2-10.8-5.5\n-12.5-2.3-1.7-7.5-2.5-15.5-2.5-14 0-21 3.7-21 11 0 2 2 10.3 6 25 20.7 83.3 67\n 151.7 139 205zm0 0v40h399900v-40z",rightbrace:"M400000 542l\n-6 6h-17c-12.7 0-19.3-.3-20-1-4-4-7.3-8.3-10-13-35.3-51.3-80.8-93.8-136.5-127.5\ns-117.2-55.8-184.5-66.5c-.7 0-2-.3-4-1-18.7-2.7-76-4.3-172-5H0V214h399571l6 1\nc124.7 8 235 61.7 331 161 31.3 33.3 59.7 72.7 85 118l7 13v35z",rightbraceunder:"M399994 0l6 6v35l-6 11c-56 104-135.3 181.3-238 232-57.3\n 28.7-117 45-179 50H-300V214h399897c43.3-7 81-15 113-26 100.7-33 179.7-91 237\n-174 2.7-5 6-9 10-13 .7-1 7.3-1 20-1h17z",rightgroup:"M0 80h399565c371 0 266.7 149.4 414 180 5.9 1.2 18 0 18 0 2 0\n 3-1 3-3v-38c-76-158-257-219-435-219H0z",rightgroupunder:"M0 262h399565c371 0 266.7-149.4 414-180 5.9-1.2 18 0 18\n 0 2 0 3 1 3 3v38c-76 158-257 219-435 219H0z",rightharpoon:"M0 241v40h399993c4.7-4.7 7-9.3 7-14 0-9.3\n-3.7-15.3-11-18-92.7-56.7-159-133.7-199-231-3.3-9.3-6-14.7-8-16-2-1.3-7-2-15-2\n-10.7 0-16.7 2-18 6-2 2.7-1 9.7 3 21 15.3 42 36.7 81.8 64 119.5 27.3 37.7 58\n 69.2 92 94.5zm0 0v40h399900v-40z",rightharpoonplus:"M0 241v40h399993c4.7-4.7 7-9.3 7-14 0-9.3-3.7-15.3-11\n-18-92.7-56.7-159-133.7-199-231-3.3-9.3-6-14.7-8-16-2-1.3-7-2-15-2-10.7 0-16.7\n 2-18 6-2 2.7-1 9.7 3 21 15.3 42 36.7 81.8 64 119.5 27.3 37.7 58 69.2 92 94.5z\nm0 0v40h399900v-40z m100 194v40h399900v-40zm0 0v40h399900v-40z",rightharpoondown:"M399747 511c0 7.3 6.7 11 20 11 8 0 13-.8 15-2.5s4.7-6.8\n 8-15.5c40-94 99.3-166.3 178-217 13.3-8 20.3-12.3 21-13 5.3-3.3 8.5-5.8 9.5\n-7.5 1-1.7 1.5-5.2 1.5-10.5s-2.3-10.3-7-15H0v40h399908c-34 25.3-64.7 57-92 95\n-27.3 38-48.7 77.7-64 119-3.3 8.7-5 14-5 16zM0 241v40h399900v-40z",rightharpoondownplus:"M399747 705c0 7.3 6.7 11 20 11 8 0 13-.8\n 15-2.5s4.7-6.8 8-15.5c40-94 99.3-166.3 178-217 13.3-8 20.3-12.3 21-13 5.3-3.3\n 8.5-5.8 9.5-7.5 1-1.7 1.5-5.2 1.5-10.5s-2.3-10.3-7-15H0v40h399908c-34 25.3\n-64.7 57-92 95-27.3 38-48.7 77.7-64 119-3.3 8.7-5 14-5 16zM0 435v40h399900v-40z\nm0-194v40h400000v-40zm0 0v40h400000v-40z",righthook:"M399859 241c-764 0 0 0 0 0 40-3.3 68.7-15.7 86-37 10-12 15-25.3\n 15-40 0-22.7-9.8-40.7-29.5-54-19.7-13.3-43.5-21-71.5-23-17.3-1.3-26-8-26-20 0\n-13.3 8.7-20 26-20 38 0 71 11.2 99 33.5 0 0 7 5.6 21 16.7 14 11.2 21 33.5 21\n 66.8s-14 61.2-42 83.5c-28 22.3-61 33.5-99 33.5L0 241z M0 281v-40h399859v40z",rightlinesegment:"M399960 241 V94 h40 V428 h-40 V281 H0 v-40z\nM399960 241 V94 h40 V428 h-40 V281 H0 v-40z",rightToFrom:"M400000 167c-70.7-42-118-97.7-142-167h-23c-15.3 0-23 .3-23\n 1 0 1.3 5.3 13.7 16 37 18 35.3 41.3 69 70 101l7 8H0v40h399905l-7 8c-28.7 32\n-52 65.7-70 101-10.7 23.3-16 35.7-16 37 0 .7 7.7 1 23 1h23c24-69.3 71.3-125 142\n-167z M100 147v40h399900v-40zM0 341v40h399900v-40z",twoheadleftarrow:"M0 167c68 40\n 115.7 95.7 143 167h22c15.3 0 23-.3 23-1 0-1.3-5.3-13.7-16-37-18-35.3-41.3-69\n-70-101l-7-8h125l9 7c50.7 39.3 85 86 103 140h46c0-4.7-6.3-18.7-19-42-18-35.3\n-40-67.3-66-96l-9-9h399716v-40H284l9-9c26-28.7 48-60.7 66-96 12.7-23.333 19\n-37.333 19-42h-46c-18 54-52.3 100.7-103 140l-9 7H95l7-8c28.7-32 52-65.7 70-101\n 10.7-23.333 16-35.7 16-37 0-.7-7.7-1-23-1h-22C115.7 71.3 68 127 0 167z",twoheadrightarrow:"M400000 167\nc-68-40-115.7-95.7-143-167h-22c-15.3 0-23 .3-23 1 0 1.3 5.3 13.7 16 37 18 35.3\n 41.3 69 70 101l7 8h-125l-9-7c-50.7-39.3-85-86-103-140h-46c0 4.7 6.3 18.7 19 42\n 18 35.3 40 67.3 66 96l9 9H0v40h399716l-9 9c-26 28.7-48 60.7-66 96-12.7 23.333\n-19 37.333-19 42h46c18-54 52.3-100.7 103-140l9-7h125l-7 8c-28.7 32-52 65.7-70\n 101-10.7 23.333-16 35.7-16 37 0 .7 7.7 1 23 1h22c27.3-71.3 75-127 143-167z",tilde1:"M200 55.538c-77 0-168 73.953-177 73.953-3 0-7\n-2.175-9-5.437L2 97c-1-2-2-4-2-6 0-4 2-7 5-9l20-12C116 12 171 0 207 0c86 0\n 114 68 191 68 78 0 168-68 177-68 4 0 7 2 9 5l12 19c1 2.175 2 4.35 2 6.525 0\n 4.35-2 7.613-5 9.788l-19 13.05c-92 63.077-116.937 75.308-183 76.128\n-68.267.847-113-73.952-191-73.952z",tilde2:"M344 55.266c-142 0-300.638 81.316-311.5 86.418\n-8.01 3.762-22.5 10.91-23.5 5.562L1 120c-1-2-1-3-1-4 0-5 3-9 8-10l18.4-9C160.9\n 31.9 283 0 358 0c148 0 188 122 331 122s314-97 326-97c4 0 8 2 10 7l7 21.114\nc1 2.14 1 3.21 1 4.28 0 5.347-3 9.626-7 10.696l-22.3 12.622C852.6 158.372 751\n 181.476 676 181.476c-149 0-189-126.21-332-126.21z",tilde3:"M786 59C457 59 32 175.242 13 175.242c-6 0-10-3.457\n-11-10.37L.15 138c-1-7 3-12 10-13l19.2-6.4C378.4 40.7 634.3 0 804.3 0c337 0\n 411.8 157 746.8 157 328 0 754-112 773-112 5 0 10 3 11 9l1 14.075c1 8.066-.697\n 16.595-6.697 17.492l-21.052 7.31c-367.9 98.146-609.15 122.696-778.15 122.696\n -338 0-409-156.573-744-156.573z",tilde4:"M786 58C457 58 32 177.487 13 177.487c-6 0-10-3.345\n-11-10.035L.15 143c-1-7 3-12 10-13l22-6.7C381.2 35 637.15 0 807.15 0c337 0 409\n 177 744 177 328 0 754-127 773-127 5 0 10 3 11 9l1 14.794c1 7.805-3 13.38-9\n 14.495l-20.7 5.574c-366.85 99.79-607.3 139.372-776.3 139.372-338 0-409\n -175.236-744-175.236z",vec:"M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5\n3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11\n10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63\n-1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1\n-7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59\nH213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359\nc-16-25.333-24-45-24-59z",widehat1:"M529 0h5l519 115c5 1 9 5 9 10 0 1-1 2-1 3l-4 22\nc-1 5-5 9-11 9h-2L532 67 19 159h-2c-5 0-9-4-11-9l-5-22c-1-6 2-12 8-13z",widehat2:"M1181 0h2l1171 176c6 0 10 5 10 11l-2 23c-1 6-5 10\n-11 10h-1L1182 67 15 220h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z",widehat3:"M1181 0h2l1171 236c6 0 10 5 10 11l-2 23c-1 6-5 10\n-11 10h-1L1182 67 15 280h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z",widehat4:"M1181 0h2l1171 296c6 0 10 5 10 11l-2 23c-1 6-5 10\n-11 10h-1L1182 67 15 340h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z",widecheck1:"M529,159h5l519,-115c5,-1,9,-5,9,-10c0,-1,-1,-2,-1,-3l-4,-22c-1,\n-5,-5,-9,-11,-9h-2l-512,92l-513,-92h-2c-5,0,-9,4,-11,9l-5,22c-1,6,2,12,8,13z",widecheck2:"M1181,220h2l1171,-176c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10,\n-11,-10h-1l-1168,153l-1167,-153h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z",widecheck3:"M1181,280h2l1171,-236c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10,\n-11,-10h-1l-1168,213l-1167,-213h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z",widecheck4:"M1181,340h2l1171,-296c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10,\n-11,-10h-1l-1168,273l-1167,-273h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z",baraboveleftarrow:"M400000 620h-399890l3 -3c68.7 -52.7 113.7 -120 135 -202\nc4 -14.7 6 -23 6 -25c0 -7.3 -7 -11 -21 -11c-8 0 -13.2 0.8 -15.5 2.5\nc-2.3 1.7 -4.2 5.8 -5.5 12.5c-1.3 4.7 -2.7 10.3 -4 17c-12 48.7 -34.8 92 -68.5 130\ns-74.2 66.3 -121.5 85c-10 4 -16 7.7 -18 11c0 8.7 6 14.3 18 17c47.3 18.7 87.8 47\n121.5 85s56.5 81.3 68.5 130c0.7 2 1.3 5 2 9s1.2 6.7 1.5 8c0.3 1.3 1 3.3 2 6\ns2.2 4.5 3.5 5.5c1.3 1 3.3 1.8 6 2.5s6 1 10 1c14 0 21 -3.7 21 -11\nc0 -2 -2 -10.3 -6 -25c-20 -79.3 -65 -146.7 -135 -202l-3 -3h399890z\nM100 620v40h399900v-40z M0 241v40h399900v-40zM0 241v40h399900v-40z",rightarrowabovebar:"M0 241v40h399891c-47.3 35.3-84 78-110 128-16.7 32\n-27.7 63.7-33 95 0 1.3-.2 2.7-.5 4-.3 1.3-.5 2.3-.5 3 0 7.3 6.7 11 20 11 8 0\n13.2-.8 15.5-2.5 2.3-1.7 4.2-5.5 5.5-11.5 2-13.3 5.7-27 11-41 14.7-44.7 39\n-84.5 73-119.5s73.7-60.2 119-75.5c6-2 9-5.7 9-11s-3-9-9-11c-45.3-15.3-85-40.5\n-119-75.5s-58.3-74.8-73-119.5c-4.7-14-8.3-27.3-11-40-1.3-6.7-3.2-10.8-5.5\n-12.5-2.3-1.7-7.5-2.5-15.5-2.5-14 0-21 3.7-21 11 0 2 2 10.3 6 25 20.7 83.3 67\n151.7 139 205zm96 379h399894v40H0zm0 0h399904v40H0z",baraboveshortleftharpoon:"M507,435c-4,4,-6.3,8.7,-7,14c0,5.3,0.7,9,2,11\nc1.3,2,5.3,5.3,12,10c90.7,54,156,130,196,228c3.3,10.7,6.3,16.3,9,17\nc2,0.7,5,1,9,1c0,0,5,0,5,0c10.7,0,16.7,-2,18,-6c2,-2.7,1,-9.7,-3,-21\nc-32,-87.3,-82.7,-157.7,-152,-211c0,0,-3,-3,-3,-3l399351,0l0,-40\nc-398570,0,-399437,0,-399437,0z M593 435 v40 H399500 v-40z\nM0 281 v-40 H399908 v40z M0 281 v-40 H399908 v40z",rightharpoonaboveshortbar:"M0,241 l0,40c399126,0,399993,0,399993,0\nc4.7,-4.7,7,-9.3,7,-14c0,-9.3,-3.7,-15.3,-11,-18c-92.7,-56.7,-159,-133.7,-199,\n-231c-3.3,-9.3,-6,-14.7,-8,-16c-2,-1.3,-7,-2,-15,-2c-10.7,0,-16.7,2,-18,6\nc-2,2.7,-1,9.7,3,21c15.3,42,36.7,81.8,64,119.5c27.3,37.7,58,69.2,92,94.5z\nM0 241 v40 H399908 v-40z M0 475 v-40 H399500 v40z M0 475 v-40 H399500 v40z",shortbaraboveleftharpoon:"M7,435c-4,4,-6.3,8.7,-7,14c0,5.3,0.7,9,2,11\nc1.3,2,5.3,5.3,12,10c90.7,54,156,130,196,228c3.3,10.7,6.3,16.3,9,17c2,0.7,5,1,9,\n1c0,0,5,0,5,0c10.7,0,16.7,-2,18,-6c2,-2.7,1,-9.7,-3,-21c-32,-87.3,-82.7,-157.7,\n-152,-211c0,0,-3,-3,-3,-3l399907,0l0,-40c-399126,0,-399993,0,-399993,0z\nM93 435 v40 H400000 v-40z M500 241 v40 H400000 v-40z M500 241 v40 H400000 v-40z",shortrightharpoonabovebar:"M53,241l0,40c398570,0,399437,0,399437,0\nc4.7,-4.7,7,-9.3,7,-14c0,-9.3,-3.7,-15.3,-11,-18c-92.7,-56.7,-159,-133.7,-199,\n-231c-3.3,-9.3,-6,-14.7,-8,-16c-2,-1.3,-7,-2,-15,-2c-10.7,0,-16.7,2,-18,6\nc-2,2.7,-1,9.7,3,21c15.3,42,36.7,81.8,64,119.5c27.3,37.7,58,69.2,92,94.5z\nM500 241 v40 H399408 v-40z M500 435 v40 H400000 v-40z"},A=function(){function t(t){this.children=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.maxFontSize=void 0,this.style=void 0,this.children=t,this.classes=[],this.height=0,this.depth=0,this.maxFontSize=0,this.style={}}var e=t.prototype;return e.hasClass=function(t){return c.contains(this.classes,t)},e.toNode=function(){for(var t=document.createDocumentFragment(),e=0;e"},N=function(){function t(t,e,r,a){this.children=void 0,this.attributes=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.width=void 0,this.maxFontSize=void 0,this.style=void 0,B.call(this,t,r,a),this.children=e||[]}var e=t.prototype;return e.setAttribute=function(t,e){this.attributes[t]=e},e.hasClass=function(t){return c.contains(this.classes,t)},e.toNode=function(){return C.call(this,"span")},e.toMarkup=function(){return q.call(this,"span")},t}(),O=function(){function t(t,e,r,a){this.children=void 0,this.attributes=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.maxFontSize=void 0,this.style=void 0,B.call(this,e,a),this.children=r||[],this.setAttribute("href",t)}var e=t.prototype;return e.setAttribute=function(t,e){this.attributes[t]=e},e.hasClass=function(t){return c.contains(this.classes,t)},e.toNode=function(){return C.call(this,"a")},e.toMarkup=function(){return q.call(this,"a")},t}(),I=function(){function t(t,e,r){this.src=void 0,this.alt=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.maxFontSize=void 0,this.style=void 0,this.alt=e,this.src=t,this.classes=["mord"],this.style=r}var e=t.prototype;return e.hasClass=function(t){return c.contains(this.classes,t)},e.toNode=function(){var t=document.createElement("img");for(var e in t.src=this.src,t.alt=this.alt,t.className="mord",this.style)this.style.hasOwnProperty(e)&&(t.style[e]=this.style[e]);return t},e.toMarkup=function(){var t=""+this.alt+"=n[0]&&t<=n[1])return r.name}return null}(this.text.charCodeAt(0));h&&this.classes.push(h+"_fallback"),/[îïíì]/.test(this.text)&&(this.text=R[this.text])}var e=t.prototype;return e.hasClass=function(t){return c.contains(this.classes,t)},e.toNode=function(){var t=document.createTextNode(this.text),e=null;for(var r in this.italic>0&&((e=document.createElement("span")).style.marginRight=this.italic+"em"),this.classes.length>0&&((e=e||document.createElement("span")).className=T(this.classes)),this.style)this.style.hasOwnProperty(r)&&((e=e||document.createElement("span")).style[r]=this.style[r]);return e?(e.appendChild(t),e):t},e.toMarkup=function(){var t=!1,e="0&&(r+="margin-right:"+this.italic+"em;"),this.style)this.style.hasOwnProperty(a)&&(r+=c.hyphenate(a)+":"+this.style[a]+";");r&&(t=!0,e+=' style="'+c.escape(r)+'"');var n=c.escape(this.text);return t?(e+=">",e+=n,e+=""):n},t}(),L=function(){function t(t,e){this.children=void 0,this.attributes=void 0,this.children=t||[],this.attributes=e||{}}var e=t.prototype;return e.toNode=function(){var t=document.createElementNS("http://www.w3.org/2000/svg","svg");for(var e in this.attributes)Object.prototype.hasOwnProperty.call(this.attributes,e)&&t.setAttribute(e,this.attributes[e]);for(var r=0;r":""},t}(),H=function(){function t(t){this.attributes=void 0,this.attributes=t||{}}var e=t.prototype;return e.toNode=function(){var t=document.createElementNS("http://www.w3.org/2000/svg","line");for(var e in this.attributes)Object.prototype.hasOwnProperty.call(this.attributes,e)&&t.setAttribute(e,this.attributes[e]);return t},e.toMarkup=function(){var t="",">"),j("math",Z,et,":",":"),j("math",Z,et,"≈","\\approx",!0),j("math",Z,et,"≅","\\cong",!0),j("math",Z,et,"≥","\\ge"),j("math",Z,et,"≥","\\geq",!0),j("math",Z,et,"←","\\gets"),j("math",Z,et,">","\\gt"),j("math",Z,et,"∈","\\in",!0),j("math",Z,et,"","\\@not"),j("math",Z,et,"⊂","\\subset",!0),j("math",Z,et,"⊃","\\supset",!0),j("math",Z,et,"⊆","\\subseteq",!0),j("math",Z,et,"⊇","\\supseteq",!0),j("math",K,et,"⊈","\\nsubseteq",!0),j("math",K,et,"⊉","\\nsupseteq",!0),j("math",Z,et,"⊨","\\models"),j("math",Z,et,"←","\\leftarrow",!0),j("math",Z,et,"≤","\\le"),j("math",Z,et,"≤","\\leq",!0),j("math",Z,et,"<","\\lt"),j("math",Z,et,"→","\\rightarrow",!0),j("math",Z,et,"→","\\to"),j("math",K,et,"≱","\\ngeq",!0),j("math",K,et,"≰","\\nleq",!0),j("math",Z,"spacing"," ","\\ "),j("math",Z,"spacing"," ","~"),j("math",Z,"spacing"," ","\\space"),j("math",Z,"spacing"," ","\\nobreakspace"),j("text",Z,"spacing"," ","\\ "),j("text",Z,"spacing"," ","~"),j("text",Z,"spacing"," ","\\space"),j("text",Z,"spacing"," ","\\nobreakspace"),j("math",Z,"spacing",null,"\\nobreak"),j("math",Z,"spacing",null,"\\allowbreak"),j("math",Z,"punct",",",","),j("math",Z,"punct",";",";"),j("math",K,J,"⊼","\\barwedge",!0),j("math",K,J,"⊻","\\veebar",!0),j("math",Z,J,"⊙","\\odot",!0),j("math",Z,J,"⊕","\\oplus",!0),j("math",Z,J,"⊗","\\otimes",!0),j("math",Z,"textord","∂","\\partial",!0),j("math",Z,J,"⊘","\\oslash",!0),j("math",K,J,"⊚","\\circledcirc",!0),j("math",K,J,"⊡","\\boxdot",!0),j("math",Z,J,"△","\\bigtriangleup"),j("math",Z,J,"▽","\\bigtriangledown"),j("math",Z,J,"†","\\dagger"),j("math",Z,J,"⋄","\\diamond"),j("math",Z,J,"⋆","\\star"),j("math",Z,J,"◃","\\triangleleft"),j("math",Z,J,"▹","\\triangleright"),j("math",Z,"open","{","\\{"),j("text",Z,"textord","{","\\{"),j("text",Z,"textord","{","\\textbraceleft"),j("math",Z,"close","}","\\}"),j("text",Z,"textord","}","\\}"),j("text",Z,"textord","}","\\textbraceright"),j("math",Z,"open","{","\\lbrace"),j("math",Z,"close","}","\\rbrace"),j("math",Z,"open","[","\\lbrack"),j("text",Z,"textord","[","\\lbrack"),j("math",Z,"close","]","\\rbrack"),j("text",Z,"textord","]","\\rbrack"),j("math",Z,"open","(","\\lparen"),j("math",Z,"close",")","\\rparen"),j("text",Z,"textord","<","\\textless"),j("text",Z,"textord",">","\\textgreater"),j("math",Z,"open","⌊","\\lfloor",!0),j("math",Z,"close","⌋","\\rfloor",!0),j("math",Z,"open","⌈","\\lceil",!0),j("math",Z,"close","⌉","\\rceil",!0),j("math",Z,"textord","\\","\\backslash"),j("math",Z,"textord","∣","|"),j("math",Z,"textord","∣","\\vert"),j("text",Z,"textord","|","\\textbar"),j("math",Z,"textord","∥","\\|"),j("math",Z,"textord","∥","\\Vert"),j("text",Z,"textord","∥","\\textbardbl"),j("text",Z,"textord","~","\\textasciitilde"),j("text",Z,"textord","\\","\\textbackslash"),j("text",Z,"textord","^","\\textasciicircum"),j("math",Z,et,"↑","\\uparrow",!0),j("math",Z,et,"⇑","\\Uparrow",!0),j("math",Z,et,"↓","\\downarrow",!0),j("math",Z,et,"⇓","\\Downarrow",!0),j("math",Z,et,"↕","\\updownarrow",!0),j("math",Z,et,"⇕","\\Updownarrow",!0),j("math",Z,tt,"∐","\\coprod"),j("math",Z,tt,"⋁","\\bigvee"),j("math",Z,tt,"⋀","\\bigwedge"),j("math",Z,tt,"⨄","\\biguplus"),j("math",Z,tt,"⋂","\\bigcap"),j("math",Z,tt,"⋃","\\bigcup"),j("math",Z,tt,"∫","\\int"),j("math",Z,tt,"∫","\\intop"),j("math",Z,tt,"∬","\\iint"),j("math",Z,tt,"∭","\\iiint"),j("math",Z,tt,"∏","\\prod"),j("math",Z,tt,"∑","\\sum"),j("math",Z,tt,"⨂","\\bigotimes"),j("math",Z,tt,"⨁","\\bigoplus"),j("math",Z,tt,"⨀","\\bigodot"),j("math",Z,tt,"∮","\\oint"),j("math",Z,tt,"∯","\\oiint"),j("math",Z,tt,"∰","\\oiiint"),j("math",Z,tt,"⨆","\\bigsqcup"),j("math",Z,tt,"∫","\\smallint"),j("text",Z,"inner","…","\\textellipsis"),j("math",Z,"inner","…","\\mathellipsis"),j("text",Z,"inner","…","\\ldots",!0),j("math",Z,"inner","…","\\ldots",!0),j("math",Z,"inner","⋯","\\@cdots",!0),j("math",Z,"inner","⋱","\\ddots",!0),j("math",Z,"textord","⋮","\\varvdots"),j("math",Z,"accent-token","ˊ","\\acute"),j("math",Z,"accent-token","ˋ","\\grave"),j("math",Z,"accent-token","¨","\\ddot"),j("math",Z,"accent-token","~","\\tilde"),j("math",Z,"accent-token","ˉ","\\bar"),j("math",Z,"accent-token","˘","\\breve"),j("math",Z,"accent-token","ˇ","\\check"),j("math",Z,"accent-token","^","\\hat"),j("math",Z,"accent-token","⃗","\\vec"),j("math",Z,"accent-token","˙","\\dot"),j("math",Z,"accent-token","˚","\\mathring"),j("math",Z,Q,"ı","\\imath",!0),j("math",Z,Q,"ȷ","\\jmath",!0),j("text",Z,"textord","ı","\\i",!0),j("text",Z,"textord","ȷ","\\j",!0),j("text",Z,"textord","ß","\\ss",!0),j("text",Z,"textord","æ","\\ae",!0),j("text",Z,"textord","æ","\\ae",!0),j("text",Z,"textord","œ","\\oe",!0),j("text",Z,"textord","ø","\\o",!0),j("text",Z,"textord","Æ","\\AE",!0),j("text",Z,"textord","Œ","\\OE",!0),j("text",Z,"textord","Ø","\\O",!0),j("text",Z,"accent-token","ˊ","\\'"),j("text",Z,"accent-token","ˋ","\\`"),j("text",Z,"accent-token","ˆ","\\^"),j("text",Z,"accent-token","˜","\\~"),j("text",Z,"accent-token","ˉ","\\="),j("text",Z,"accent-token","˘","\\u"),j("text",Z,"accent-token","˙","\\."),j("text",Z,"accent-token","˚","\\r"),j("text",Z,"accent-token","ˇ","\\v"),j("text",Z,"accent-token","¨",'\\"'),j("text",Z,"accent-token","˝","\\H"),j("text",Z,"accent-token","◯","\\textcircled");var rt={"--":!0,"---":!0,"``":!0,"''":!0};j("text",Z,"textord","–","--"),j("text",Z,"textord","–","\\textendash"),j("text",Z,"textord","—","---"),j("text",Z,"textord","—","\\textemdash"),j("text",Z,"textord","‘","`"),j("text",Z,"textord","‘","\\textquoteleft"),j("text",Z,"textord","’","'"),j("text",Z,"textord","’","\\textquoteright"),j("text",Z,"textord","“","``"),j("text",Z,"textord","“","\\textquotedblleft"),j("text",Z,"textord","”","''"),j("text",Z,"textord","”","\\textquotedblright"),j("math",Z,"textord","°","\\degree",!0),j("text",Z,"textord","°","\\degree"),j("text",Z,"textord","°","\\textdegree",!0),j("math",Z,Q,"£","\\pounds"),j("math",Z,Q,"£","\\mathsterling",!0),j("text",Z,Q,"£","\\pounds"),j("text",Z,Q,"£","\\textsterling",!0),j("math",K,"textord","✠","\\maltese"),j("text",K,"textord","✠","\\maltese"),j("text",Z,"spacing"," ","\\ "),j("text",Z,"spacing"," "," "),j("text",Z,"spacing"," ","~");for(var at=0;at<'0123456789/@."'.length;at++){var nt='0123456789/@."'.charAt(at);j("math",Z,"textord",nt,nt)}for(var it=0;it<'0123456789!@*()-=+[]<>|";:?/.,'.length;it++){var ot='0123456789!@*()-=+[]<>|";:?/.,'.charAt(it);j("text",Z,"textord",ot,ot)}for(var st="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",ht=0;ht=5?0:t>=3?1:2]){var r=Y[e]={cssEmPerMu:V.quad[e]/18};for(var a in V)V.hasOwnProperty(a)&&(r[a]=V[a][e])}return Y[e]}(this.size)),this._fontMetrics},e.getColor=function(){return this.phantom?"transparent":this.color},t}();kt.BASESIZE=6;var St=kt,Mt={pt:1,mm:7227/2540,cm:7227/254,in:72.27,bp:1.00375,pc:12,dd:1238/1157,cc:14856/1157,nd:685/642,nc:1370/107,sp:1/65536,px:1.00375},zt={ex:!0,em:!0,mu:!0},At=function(t){return"string"!=typeof t&&(t=t.unit),t in Mt||t in zt||"ex"===t},Tt=function(t,e){var r;if(t.unit in Mt)r=Mt[t.unit]/e.fontMetrics().ptPerEm/e.sizeMultiplier;else if("mu"===t.unit)r=e.fontMetrics().cssEmPerMu;else{var a;if(a=e.style.isTight()?e.havingStyle(e.style.text()):e,"ex"===t.unit)r=a.fontMetrics().xHeight;else{if("em"!==t.unit)throw new o("Invalid unit: '"+t.unit+"'");r=a.fontMetrics().quad}a!==e&&(r*=a.sizeMultiplier/e.sizeMultiplier)}return Math.min(t.number*r,e.maxSize)},Bt=["\\imath","ı","\\jmath","ȷ","\\pounds","\\mathsterling","\\textsterling","£"],Ct=function(t,e,r){return $[r][t]&&$[r][t].replace&&(t=$[r][t].replace),{value:t,metrics:G(t,e,r)}},qt=function(t,e,r,a,n){var i,o=Ct(t,e,r),s=o.metrics;if(t=o.value,s){var h=s.italic;("text"===r||a&&"mathit"===a.font)&&(h=0),i=new E(t,s.height,s.depth,h,s.skew,s.width,n)}else"undefined"!=typeof console&&console.warn("No character metrics for '"+t+"' in style '"+e+"' and mode '"+r+"'"),i=new E(t,0,0,0,0,0,n);if(a){i.maxFontSize=a.sizeMultiplier,a.style.isTight()&&i.classes.push("mtight");var l=a.getColor();l&&(i.style.color=l)}return i},Nt=function(t,e){if(T(t.classes)!==T(e.classes)||t.skew!==e.skew||t.maxFontSize!==e.maxFontSize)return!1;for(var r in t.style)if(t.style.hasOwnProperty(r)&&t.style[r]!==e.style[r])return!1;for(var a in e.style)if(e.style.hasOwnProperty(a)&&t.style[a]!==e.style[a])return!1;return!0},Ot=function(t){for(var e=0,r=0,a=0,n=0;ne&&(e=i.height),i.depth>r&&(r=i.depth),i.maxFontSize>a&&(a=i.maxFontSize)}t.height=e,t.depth=r,t.maxFontSize=a},It=function(t,e,r,a){var n=new N(t,e,r,a);return Ot(n),n},Rt=function(t,e,r,a){return new N(t,e,r,a)},Et=function(t){var e=new A(t);return Ot(e),e},Lt=function(t,e,r){var a="";switch(t){case"amsrm":a="AMS";break;case"textrm":a="Main";break;case"textsf":a="SansSerif";break;case"texttt":a="Typewriter";break;default:a=t}return a+"-"+("textbf"===e&&"textit"===r?"BoldItalic":"textbf"===e?"Bold":"textit"===e?"Italic":"Regular")},Pt={mathbf:{variant:"bold",fontName:"Main-Bold"},mathrm:{variant:"normal",fontName:"Main-Regular"},textit:{variant:"italic",fontName:"Main-Italic"},mathit:{variant:"italic",fontName:"Main-Italic"},mathbb:{variant:"double-struck",fontName:"AMS-Regular"},mathcal:{variant:"script",fontName:"Caligraphic-Regular"},mathfrak:{variant:"fraktur",fontName:"Fraktur-Regular"},mathscr:{variant:"script",fontName:"Script-Regular"},mathsf:{variant:"sans-serif",fontName:"SansSerif-Regular"},mathtt:{variant:"monospace",fontName:"Typewriter-Regular"}},Ht={vec:["vec",.471,.714],oiintSize1:["oiintSize1",.957,.499],oiintSize2:["oiintSize2",1.472,.659],oiiintSize1:["oiiintSize1",1.304,.499],oiiintSize2:["oiiintSize2",1.98,.659]},Dt={fontMap:Pt,makeSymbol:qt,mathsym:function(t,e,r,a){return void 0===a&&(a=[]),"boldsymbol"===r.font&&Ct(t,"Main-Bold",e).metrics?qt(t,"Main-Bold",e,r,a.concat(["mathbf"])):"\\"===t||"main"===$[e][t].font?qt(t,"Main-Regular",e,r,a):qt(t,"AMS-Regular",e,r,a.concat(["amsrm"]))},makeSpan:It,makeSvgSpan:Rt,makeLineSpan:function(t,e,r){var a=It([t],[],e);return a.height=Math.max(r||e.fontMetrics().defaultRuleThickness,e.minRuleThickness),a.style.borderBottomWidth=a.height+"em",a.maxFontSize=1,a},makeAnchor:function(t,e,r,a){var n=new O(t,e,r,a);return Ot(n),n},makeFragment:Et,wrapFragment:function(t,e){return t instanceof A?It([],[t],e):t},makeVList:function(t,e){for(var r=function(t){if("individualShift"===t.positionType){for(var e=t.children,r=[e[0]],a=-e[0].shift-e[0].elem.depth,n=a,i=1;i0&&(i.push(pe(o,e)),o=[]),i.push(n[s]));o.length>0&&i.push(pe(o,e)),r&&((a=pe(se(r,e,!0))).classes=["tag"],i.push(a));var l=re(["katex-html"],i);if(l.setAttribute("aria-hidden","true"),a){var m=a.children[0];m.style.height=l.height+l.depth+"em",m.style.verticalAlign=-l.depth+"em"}return l}function fe(t){return new A(t)}var ge=function(){function t(t,e){this.type=void 0,this.attributes=void 0,this.children=void 0,this.type=t,this.attributes={},this.children=e||[]}var e=t.prototype;return e.setAttribute=function(t,e){this.attributes[t]=e},e.getAttribute=function(t){return this.attributes[t]},e.toNode=function(){var t=document.createElementNS("http://www.w3.org/1998/Math/MathML",this.type);for(var e in this.attributes)Object.prototype.hasOwnProperty.call(this.attributes,e)&&t.setAttribute(e,this.attributes[e]);for(var r=0;r"},e.toText=function(){return this.children.map((function(t){return t.toText()})).join("")},t}(),xe=function(){function t(t){this.text=void 0,this.text=t}var e=t.prototype;return e.toNode=function(){return document.createTextNode(this.text)},e.toMarkup=function(){return c.escape(this.toText())},e.toText=function(){return this.text},t}(),ve={MathNode:ge,TextNode:xe,SpaceNode:function(){function t(t){this.width=void 0,this.character=void 0,this.width=t,this.character=t>=.05555&&t<=.05556?" ":t>=.1666&&t<=.1667?" ":t>=.2222&&t<=.2223?" ":t>=.2777&&t<=.2778?"  ":t>=-.05556&&t<=-.05555?" ⁣":t>=-.1667&&t<=-.1666?" ⁣":t>=-.2223&&t<=-.2222?" ⁣":t>=-.2778&&t<=-.2777?" ⁣":null}var e=t.prototype;return e.toNode=function(){if(this.character)return document.createTextNode(this.character);var t=document.createElementNS("http://www.w3.org/1998/Math/MathML","mspace");return t.setAttribute("width",this.width+"em"),t},e.toMarkup=function(){return this.character?""+this.character+"":''},e.toText=function(){return this.character?this.character:" "},t}(),newDocumentFragment:fe},be=function(t,e,r){return!$[e][t]||!$[e][t].replace||55349===t.charCodeAt(0)||rt.hasOwnProperty(t)&&r&&(r.fontFamily&&"tt"===r.fontFamily.substr(4,2)||r.font&&"tt"===r.font.substr(4,2))||(t=$[e][t].replace),new ve.TextNode(t)},ye=function(t){return 1===t.length?t[0]:new ve.MathNode("mrow",t)},we=function(t,e){if("texttt"===e.fontFamily)return"monospace";if("textsf"===e.fontFamily)return"textit"===e.fontShape&&"textbf"===e.fontWeight?"sans-serif-bold-italic":"textit"===e.fontShape?"sans-serif-italic":"textbf"===e.fontWeight?"bold-sans-serif":"sans-serif";if("textit"===e.fontShape&&"textbf"===e.fontWeight)return"bold-italic";if("textit"===e.fontShape)return"italic";if("textbf"===e.fontWeight)return"bold";var r=e.font;if(!r||"mathnormal"===r)return null;var a=t.mode;if("mathit"===r)return"italic";if("boldsymbol"===r)return"bold-italic";if("mathbf"===r)return"bold";if("mathbb"===r)return"double-struck";if("mathfrak"===r)return"fraktur";if("mathscr"===r||"mathcal"===r)return"script";if("mathsf"===r)return"sans-serif";if("mathtt"===r)return"monospace";var n=t.text;return c.contains(["\\imath","\\jmath"],n)?null:($[a][n]&&$[a][n].replace&&(n=$[a][n].replace),G(n,Dt.fontMap[r].fontName,a)?Dt.fontMap[r].variant:null)},ke=function(t,e,r){if(1===t.length){var a=Me(t[0],e);return r&&a instanceof ge&&"mo"===a.type&&(a.setAttribute("lspace","0em"),a.setAttribute("rspace","0em")),[a]}for(var n,i=[],o=0;o0&&(p.text=p.text.slice(0,1)+"̸"+p.text.slice(1),i.pop())}}}i.push(s),n=s}return i},Se=function(t,e,r){return ye(ke(t,e,r))},Me=function(t,e){if(!t)return new ve.MathNode("mrow");if(Jt[t.type])return Jt[t.type](t,e);throw new o("Got group of unknown type: '"+t.type+"'")};function ze(t,e,r,a){var n,i=ke(t,r);n=1===i.length&&i[0]instanceof ge&&c.contains(["mrow","mtable"],i[0].type)?i[0]:new ve.MathNode("mrow",i);var o=new ve.MathNode("annotation",[new ve.TextNode(e)]);o.setAttribute("encoding","application/x-tex");var s=new ve.MathNode("semantics",[n,o]),h=new ve.MathNode("math",[s]);h.setAttribute("xmlns","http://www.w3.org/1998/Math/MathML");var l=a?"katex":"katex-mathml";return Dt.makeSpan([l],[h])}var Ae=function(t){return new St({style:t.displayMode?w.DISPLAY:w.TEXT,maxSize:t.maxSize,minRuleThickness:t.minRuleThickness})},Te=function(t,e){if(e.displayMode){var r=["katex-display"];e.leqno&&r.push("leqno"),e.fleqn&&r.push("fleqn"),t=Dt.makeSpan(r,[t])}return t},Be=function(t,e,r){var a,n=Ae(r);if("mathml"===r.output)return ze(t,e,n,!0);if("html"===r.output){var i=de(t,n);a=Dt.makeSpan(["katex"],[i])}else{var o=ze(t,e,n,!1),s=de(t,n);a=Dt.makeSpan(["katex"],[o,s])}return Te(a,r)},Ce={widehat:"^",widecheck:"ˇ",widetilde:"~",utilde:"~",overleftarrow:"←",underleftarrow:"←",xleftarrow:"←",overrightarrow:"→",underrightarrow:"→",xrightarrow:"→",underbrace:"⏟",overbrace:"⏞",overgroup:"⏠",undergroup:"⏡",overleftrightarrow:"↔",underleftrightarrow:"↔",xleftrightarrow:"↔",Overrightarrow:"⇒",xRightarrow:"⇒",overleftharpoon:"↼",xleftharpoonup:"↼",overrightharpoon:"⇀",xrightharpoonup:"⇀",xLeftarrow:"⇐",xLeftrightarrow:"⇔",xhookleftarrow:"↩",xhookrightarrow:"↪",xmapsto:"↦",xrightharpoondown:"⇁",xleftharpoondown:"↽",xrightleftharpoons:"⇌",xleftrightharpoons:"⇋",xtwoheadleftarrow:"↞",xtwoheadrightarrow:"↠",xlongequal:"=",xtofrom:"⇄",xrightleftarrows:"⇄",xrightequilibrium:"⇌",xleftequilibrium:"⇋"},qe={overrightarrow:[["rightarrow"],.888,522,"xMaxYMin"],overleftarrow:[["leftarrow"],.888,522,"xMinYMin"],underrightarrow:[["rightarrow"],.888,522,"xMaxYMin"],underleftarrow:[["leftarrow"],.888,522,"xMinYMin"],xrightarrow:[["rightarrow"],1.469,522,"xMaxYMin"],xleftarrow:[["leftarrow"],1.469,522,"xMinYMin"],Overrightarrow:[["doublerightarrow"],.888,560,"xMaxYMin"],xRightarrow:[["doublerightarrow"],1.526,560,"xMaxYMin"],xLeftarrow:[["doubleleftarrow"],1.526,560,"xMinYMin"],overleftharpoon:[["leftharpoon"],.888,522,"xMinYMin"],xleftharpoonup:[["leftharpoon"],.888,522,"xMinYMin"],xleftharpoondown:[["leftharpoondown"],.888,522,"xMinYMin"],overrightharpoon:[["rightharpoon"],.888,522,"xMaxYMin"],xrightharpoonup:[["rightharpoon"],.888,522,"xMaxYMin"],xrightharpoondown:[["rightharpoondown"],.888,522,"xMaxYMin"],xlongequal:[["longequal"],.888,334,"xMinYMin"],xtwoheadleftarrow:[["twoheadleftarrow"],.888,334,"xMinYMin"],xtwoheadrightarrow:[["twoheadrightarrow"],.888,334,"xMaxYMin"],overleftrightarrow:[["leftarrow","rightarrow"],.888,522],overbrace:[["leftbrace","midbrace","rightbrace"],1.6,548],underbrace:[["leftbraceunder","midbraceunder","rightbraceunder"],1.6,548],underleftrightarrow:[["leftarrow","rightarrow"],.888,522],xleftrightarrow:[["leftarrow","rightarrow"],1.75,522],xLeftrightarrow:[["doubleleftarrow","doublerightarrow"],1.75,560],xrightleftharpoons:[["leftharpoondownplus","rightharpoonplus"],1.75,716],xleftrightharpoons:[["leftharpoonplus","rightharpoondownplus"],1.75,716],xhookleftarrow:[["leftarrow","righthook"],1.08,522],xhookrightarrow:[["lefthook","rightarrow"],1.08,522],overlinesegment:[["leftlinesegment","rightlinesegment"],.888,522],underlinesegment:[["leftlinesegment","rightlinesegment"],.888,522],overgroup:[["leftgroup","rightgroup"],.888,342],undergroup:[["leftgroupunder","rightgroupunder"],.888,342],xmapsto:[["leftmapsto","rightarrow"],1.5,522],xtofrom:[["leftToFrom","rightToFrom"],1.75,528],xrightleftarrows:[["baraboveleftarrow","rightarrowabovebar"],1.75,901],xrightequilibrium:[["baraboveshortleftharpoon","rightharpoonaboveshortbar"],1.75,716],xleftequilibrium:[["shortbaraboveleftharpoon","shortrightharpoonabovebar"],1.75,716]},Ne=function(t,e,r,a){var n,i=t.height+t.depth+2*r;if(/fbox|color/.test(e)){if(n=Dt.makeSpan(["stretchy",e],[],a),"fbox"===e){var o=a.color&&a.getColor();o&&(n.style.borderColor=o)}}else{var s=[];/^[bx]cancel$/.test(e)&&s.push(new H({x1:"0",y1:"0",x2:"100%",y2:"100%","stroke-width":"0.046em"})),/^x?cancel$/.test(e)&&s.push(new H({x1:"0",y1:"100%",x2:"100%",y2:"0","stroke-width":"0.046em"}));var h=new L(s,{width:"100%",height:i+"em"});n=Dt.makeSvgSpan([],[h],a)}return n.height=i,n.style.height=i+"em",n},Oe=function(t){var e=new ve.MathNode("mo",[new ve.TextNode(Ce[t.substr(1)])]);return e.setAttribute("stretchy","true"),e},Ie=function(t,e){var r=function(){var r=4e5,a=t.label.substr(1);if(c.contains(["widehat","widecheck","widetilde","utilde"],a)){var n,i,o,s="ordgroup"===(d=t.base).type?d.body.length:1;if(s>5)"widehat"===a||"widecheck"===a?(n=420,r=2364,o=.42,i=a+"4"):(n=312,r=2340,o=.34,i="tilde4");else{var h=[1,1,2,2,3,3][s];"widehat"===a||"widecheck"===a?(r=[0,1062,2364,2364,2364][h],n=[0,239,300,360,420][h],o=[0,.24,.3,.3,.36,.42][h],i=a+h):(r=[0,600,1033,2339,2340][h],n=[0,260,286,306,312][h],o=[0,.26,.286,.3,.306,.34][h],i="tilde"+h)}var l=new P(i),m=new L([l],{width:"100%",height:o+"em",viewBox:"0 0 "+r+" "+n,preserveAspectRatio:"none"});return{span:Dt.makeSvgSpan([],[m],e),minWidth:0,height:o}}var u,p,d,f=[],g=qe[a],x=g[0],v=g[1],b=g[2],y=b/1e3,w=x.length;if(1===w)u=["hide-tail"],p=[g[3]];else if(2===w)u=["halfarrow-left","halfarrow-right"],p=["xMinYMin","xMaxYMin"];else{if(3!==w)throw new Error("Correct katexImagesData or update code here to support\n "+w+" children.");u=["brace-left","brace-center","brace-right"],p=["xMinYMin","xMidYMin","xMaxYMin"]}for(var k=0;k0&&(a.style.minWidth=n+"em"),a},Re=function(t,e){var r,a,n,i=Vt(t,"supsub");i?(r=(a=Ft(i.base,"accent")).base,i.base=r,n=function(t){if(t instanceof N)return t;throw new Error("Expected span but got "+String(t)+".")}(ue(i,e)),i.base=a):r=(a=Ft(t,"accent")).base;var o=ue(r,e.havingCrampedStyle()),s=0;if(a.isShifty&&c.isCharacterBox(r)){var h=c.getBaseElem(r);s=D(ue(h,e.havingCrampedStyle())).skew}var l,m=Math.min(o.height,e.fontMetrics().xHeight);if(a.isStretchy)l=Ie(a,e),l=Dt.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:o},{type:"elem",elem:l,wrapperClasses:["svg-align"],wrapperStyle:s>0?{width:"calc(100% - "+2*s+"em)",marginLeft:2*s+"em"}:void 0}]},e);else{var u,p;"\\vec"===a.label?(u=Dt.staticSvg("vec",e),p=Dt.svgData.vec[1]):((u=D(u=Dt.makeOrd({mode:a.mode,text:a.label},e,"textord"))).italic=0,p=u.width),l=Dt.makeSpan(["accent-body"],[u]);var d="\\textcircled"===a.label;d&&(l.classes.push("accent-full"),m=o.height);var f=s;d||(f-=p/2),l.style.left=f+"em","\\textcircled"===a.label&&(l.style.top=".2em"),l=Dt.makeVList({positionType:"firstBaseline",children:[{type:"elem",elem:o},{type:"kern",size:-m},{type:"elem",elem:l}]},e)}var g=Dt.makeSpan(["mord","accent"],[l],e);return n?(n.children[0]=g,n.height=Math.max(g.height,n.height),n.classes[0]="mord",n):g},Ee=function(t,e){var r=t.isStretchy?Oe(t.label):new ve.MathNode("mo",[be(t.label,t.mode)]),a=new ve.MathNode("mover",[Me(t.base,e),r]);return a.setAttribute("accent","true"),a},Le=new RegExp(["\\acute","\\grave","\\ddot","\\tilde","\\bar","\\breve","\\check","\\hat","\\vec","\\dot","\\mathring"].map((function(t){return"\\"+t})).join("|"));Qt({type:"accent",names:["\\acute","\\grave","\\ddot","\\tilde","\\bar","\\breve","\\check","\\hat","\\vec","\\dot","\\mathring","\\widecheck","\\widehat","\\widetilde","\\overrightarrow","\\overleftarrow","\\Overrightarrow","\\overleftrightarrow","\\overgroup","\\overlinesegment","\\overleftharpoon","\\overrightharpoon"],props:{numArgs:1},handler:function(t,e){var r=e[0],a=!Le.test(t.funcName),n=!a||"\\widehat"===t.funcName||"\\widetilde"===t.funcName||"\\widecheck"===t.funcName;return{type:"accent",mode:t.parser.mode,label:t.funcName,isStretchy:a,isShifty:n,base:r}},htmlBuilder:Re,mathmlBuilder:Ee}),Qt({type:"accent",names:["\\'","\\`","\\^","\\~","\\=","\\u","\\.",'\\"',"\\r","\\H","\\v","\\textcircled"],props:{numArgs:1,allowedInText:!0,allowedInMath:!1},handler:function(t,e){var r=e[0];return{type:"accent",mode:t.parser.mode,label:t.funcName,isStretchy:!1,isShifty:!0,base:r}},htmlBuilder:Re,mathmlBuilder:Ee}),Qt({type:"accentUnder",names:["\\underleftarrow","\\underrightarrow","\\underleftrightarrow","\\undergroup","\\underlinesegment","\\utilde"],props:{numArgs:1},handler:function(t,e){var r=t.parser,a=t.funcName,n=e[0];return{type:"accentUnder",mode:r.mode,label:a,base:n}},htmlBuilder:function(t,e){var r=ue(t.base,e),a=Ie(t,e),n="\\utilde"===t.label?.12:0,i=Dt.makeVList({positionType:"bottom",positionData:a.height+n,children:[{type:"elem",elem:a,wrapperClasses:["svg-align"]},{type:"kern",size:n},{type:"elem",elem:r}]},e);return Dt.makeSpan(["mord","accentunder"],[i],e)},mathmlBuilder:function(t,e){var r=Oe(t.label),a=new ve.MathNode("munder",[Me(t.base,e),r]);return a.setAttribute("accentunder","true"),a}});var Pe=function(t){var e=new ve.MathNode("mpadded",t?[t]:[]);return e.setAttribute("width","+0.6em"),e.setAttribute("lspace","0.3em"),e};Qt({type:"xArrow",names:["\\xleftarrow","\\xrightarrow","\\xLeftarrow","\\xRightarrow","\\xleftrightarrow","\\xLeftrightarrow","\\xhookleftarrow","\\xhookrightarrow","\\xmapsto","\\xrightharpoondown","\\xrightharpoonup","\\xleftharpoondown","\\xleftharpoonup","\\xrightleftharpoons","\\xleftrightharpoons","\\xlongequal","\\xtwoheadrightarrow","\\xtwoheadleftarrow","\\xtofrom","\\xrightleftarrows","\\xrightequilibrium","\\xleftequilibrium"],props:{numArgs:1,numOptionalArgs:1},handler:function(t,e,r){var a=t.parser,n=t.funcName;return{type:"xArrow",mode:a.mode,label:n,body:e[0],below:r[0]}},htmlBuilder:function(t,e){var r,a=e.style,n=e.havingStyle(a.sup()),i=Dt.wrapFragment(ue(t.body,n,e),e);i.classes.push("x-arrow-pad"),t.below&&(n=e.havingStyle(a.sub()),(r=Dt.wrapFragment(ue(t.below,n,e),e)).classes.push("x-arrow-pad"));var o,s=Ie(t,e),h=-e.fontMetrics().axisHeight+.5*s.height,l=-e.fontMetrics().axisHeight-.5*s.height-.111;if((i.depth>.25||"\\xleftequilibrium"===t.label)&&(l-=i.depth),r){var m=-e.fontMetrics().axisHeight+r.height+.5*s.height+.111;o=Dt.makeVList({positionType:"individualShift",children:[{type:"elem",elem:i,shift:l},{type:"elem",elem:s,shift:h},{type:"elem",elem:r,shift:m}]},e)}else o=Dt.makeVList({positionType:"individualShift",children:[{type:"elem",elem:i,shift:l},{type:"elem",elem:s,shift:h}]},e);return o.children[0].children[0].children[1].classes.push("svg-align"),Dt.makeSpan(["mrel","x-arrow"],[o],e)},mathmlBuilder:function(t,e){var r,a=Oe(t.label);if(t.body){var n=Pe(Me(t.body,e));if(t.below){var i=Pe(Me(t.below,e));r=new ve.MathNode("munderover",[a,i,n])}else r=new ve.MathNode("mover",[a,n])}else if(t.below){var o=Pe(Me(t.below,e));r=new ve.MathNode("munder",[a,o])}else r=Pe(),r=new ve.MathNode("mover",[a,r]);return r}}),Qt({type:"textord",names:["\\@char"],props:{numArgs:1,allowedInText:!0},handler:function(t,e){for(var r=t.parser,a=Ft(e[0],"ordgroup").body,n="",i=0;i","\\langle","\\rangle","/","\\backslash","\\lt","\\gt"],Ke=[0,1.2,1.8,2.4,3],Je=[{type:"small",style:w.SCRIPTSCRIPT},{type:"small",style:w.SCRIPT},{type:"small",style:w.TEXT},{type:"large",size:1},{type:"large",size:2},{type:"large",size:3},{type:"large",size:4}],Qe=[{type:"small",style:w.SCRIPTSCRIPT},{type:"small",style:w.SCRIPT},{type:"small",style:w.TEXT},{type:"stack"}],tr=[{type:"small",style:w.SCRIPTSCRIPT},{type:"small",style:w.SCRIPT},{type:"small",style:w.TEXT},{type:"large",size:1},{type:"large",size:2},{type:"large",size:3},{type:"large",size:4},{type:"stack"}],er=function(t){if("small"===t.type)return"Main-Regular";if("large"===t.type)return"Size"+t.size+"-Regular";if("stack"===t.type)return"Size4-Regular";throw new Error("Add support for delim type '"+t.type+"' here.")},rr=function(t,e,r,a){for(var n=Math.min(2,3-a.style.size);ne)return r[n]}return r[r.length-1]},ar=function(t,e,r,a,n,i){var o;"<"===t||"\\lt"===t||"⟨"===t?t="\\langle":">"!==t&&"\\gt"!==t&&"⟩"!==t||(t="\\rangle"),o=c.contains(Ze,t)?Je:c.contains($e,t)?tr:Qe;var s=rr(t,e,o,a);return"small"===s.type?function(t,e,r,a,n,i){var o=Dt.makeSymbol(t,"Main-Regular",n,a),s=Ve(o,e,a,i);return r&&Ue(s,a,e),s}(t,s.style,r,a,n,i):"large"===s.type?Ge(t,s.size,r,a,n,i):We(t,e,r,a,n,i)},nr=function(t,e){var r,a,n=e.havingBaseSizing(),i=rr("\\surd",t*n.sizeMultiplier,tr,n),o=n.sizeMultiplier,s=Math.max(0,e.minRuleThickness-e.fontMetrics().sqrtRuleThickness),h=0,l=0,m=0;return"small"===i.type?(t<1?o=1:t<1.4&&(o=.7),l=(1+s)/o,(r=Xe("sqrtMain",h=(1+s+.08)/o,m=1e3+1e3*s+80,s,e)).style.minWidth="0.853em",a=.833/o):"large"===i.type?(m=1080*Ke[i.size],l=(Ke[i.size]+s)/o,h=(Ke[i.size]+s+.08)/o,(r=Xe("sqrtSize"+i.size,h,m,s,e)).style.minWidth="1.02em",a=1/o):(h=t+s+.08,l=t+s,m=Math.floor(1e3*t+s)+80,(r=Xe("sqrtTall",h,m,s,e)).style.minWidth="0.742em",a=1.056),r.height=l,r.style.height=h+"em",{span:r,advanceWidth:a,ruleWidth:(e.fontMetrics().sqrtRuleThickness+s)*o}},ir=function(t,e,r,a,n){if("<"===t||"\\lt"===t||"⟨"===t?t="\\langle":">"!==t&&"\\gt"!==t&&"⟩"!==t||(t="\\rangle"),c.contains($e,t)||c.contains(Ze,t))return Ge(t,e,!1,r,a,n);if(c.contains(je,t))return We(t,Ke[e],!1,r,a,n);throw new o("Illegal delimiter: '"+t+"'")},or=ar,sr=function(t,e,r,a,n,i){var o=a.fontMetrics().axisHeight*a.sizeMultiplier,s=5/a.fontMetrics().ptPerEm,h=Math.max(e-o,r+o),l=Math.max(h/500*901,2*h-s);return ar(t,l,!0,a,n,i)},hr={"\\bigl":{mclass:"mopen",size:1},"\\Bigl":{mclass:"mopen",size:2},"\\biggl":{mclass:"mopen",size:3},"\\Biggl":{mclass:"mopen",size:4},"\\bigr":{mclass:"mclose",size:1},"\\Bigr":{mclass:"mclose",size:2},"\\biggr":{mclass:"mclose",size:3},"\\Biggr":{mclass:"mclose",size:4},"\\bigm":{mclass:"mrel",size:1},"\\Bigm":{mclass:"mrel",size:2},"\\biggm":{mclass:"mrel",size:3},"\\Biggm":{mclass:"mrel",size:4},"\\big":{mclass:"mord",size:1},"\\Big":{mclass:"mord",size:2},"\\bigg":{mclass:"mord",size:3},"\\Bigg":{mclass:"mord",size:4}},lr=["(","\\lparen",")","\\rparen","[","\\lbrack","]","\\rbrack","\\{","\\lbrace","\\}","\\rbrace","\\lfloor","\\rfloor","⌊","⌋","\\lceil","\\rceil","⌈","⌉","<",">","\\langle","⟨","\\rangle","⟩","\\lt","\\gt","\\lvert","\\rvert","\\lVert","\\rVert","\\lgroup","\\rgroup","⟮","⟯","\\lmoustache","\\rmoustache","⎰","⎱","/","\\backslash","|","\\vert","\\|","\\Vert","\\uparrow","\\Uparrow","\\downarrow","\\Downarrow","\\updownarrow","\\Updownarrow","."];function mr(t,e){var r=Yt(t);if(r&&c.contains(lr,r.text))return r;throw new o("Invalid delimiter: '"+(r?r.text:JSON.stringify(t))+"' after '"+e.funcName+"'",t)}function cr(t){if(!t.body)throw new Error("Bug: The leftright ParseNode wasn't fully parsed.")}Qt({type:"delimsizing",names:["\\bigl","\\Bigl","\\biggl","\\Biggl","\\bigr","\\Bigr","\\biggr","\\Biggr","\\bigm","\\Bigm","\\biggm","\\Biggm","\\big","\\Big","\\bigg","\\Bigg"],props:{numArgs:1},handler:function(t,e){var r=mr(e[0],t);return{type:"delimsizing",mode:t.parser.mode,size:hr[t.funcName].size,mclass:hr[t.funcName].mclass,delim:r.text}},htmlBuilder:function(t,e){return"."===t.delim?Dt.makeSpan([t.mclass]):ir(t.delim,t.size,e,t.mode,[t.mclass])},mathmlBuilder:function(t){var e=[];"."!==t.delim&&e.push(be(t.delim,t.mode));var r=new ve.MathNode("mo",e);return"mopen"===t.mclass||"mclose"===t.mclass?r.setAttribute("fence","true"):r.setAttribute("fence","false"),r}}),Qt({type:"leftright-right",names:["\\right"],props:{numArgs:1},handler:function(t,e){var r=t.parser.gullet.macros.get("\\current@color");if(r&&"string"!=typeof r)throw new o("\\current@color set to non-string in \\right");return{type:"leftright-right",mode:t.parser.mode,delim:mr(e[0],t).text,color:r}}}),Qt({type:"leftright",names:["\\left"],props:{numArgs:1},handler:function(t,e){var r=mr(e[0],t),a=t.parser;++a.leftrightDepth;var n=a.parseExpression(!1);--a.leftrightDepth,a.expect("\\right",!1);var i=Ft(a.parseFunction(),"leftright-right");return{type:"leftright",mode:a.mode,body:n,left:r.text,right:i.delim,rightColor:i.color}},htmlBuilder:function(t,e){cr(t);for(var r,a,n=se(t.body,e,!0,["mopen","mclose"]),i=0,o=0,s=!1,h=0;h-1?"mpadded":"menclose",[Me(t.body,e)]);switch(t.label){case"\\cancel":a.setAttribute("notation","updiagonalstrike");break;case"\\bcancel":a.setAttribute("notation","downdiagonalstrike");break;case"\\sout":a.setAttribute("notation","horizontalstrike");break;case"\\fbox":a.setAttribute("notation","box");break;case"\\fcolorbox":case"\\colorbox":if(r=e.fontMetrics().fboxsep*e.fontMetrics().ptPerEm,a.setAttribute("width","+"+2*r+"pt"),a.setAttribute("height","+"+2*r+"pt"),a.setAttribute("lspace",r+"pt"),a.setAttribute("voffset",r+"pt"),"\\fcolorbox"===t.label){var n=Math.max(e.fontMetrics().fboxrule,e.minRuleThickness);a.setAttribute("style","border: "+n+"em solid "+String(t.borderColor))}break;case"\\xcancel":a.setAttribute("notation","updiagonalstrike downdiagonalstrike")}return t.backgroundColor&&a.setAttribute("mathbackground",t.backgroundColor),a};Qt({type:"enclose",names:["\\colorbox"],props:{numArgs:2,allowedInText:!0,greediness:3,argTypes:["color","text"]},handler:function(t,e,r){var a=t.parser,n=t.funcName,i=Ft(e[0],"color-token").color,o=e[1];return{type:"enclose",mode:a.mode,label:n,backgroundColor:i,body:o}},htmlBuilder:ur,mathmlBuilder:pr}),Qt({type:"enclose",names:["\\fcolorbox"],props:{numArgs:3,allowedInText:!0,greediness:3,argTypes:["color","color","text"]},handler:function(t,e,r){var a=t.parser,n=t.funcName,i=Ft(e[0],"color-token").color,o=Ft(e[1],"color-token").color,s=e[2];return{type:"enclose",mode:a.mode,label:n,backgroundColor:o,borderColor:i,body:s}},htmlBuilder:ur,mathmlBuilder:pr}),Qt({type:"enclose",names:["\\fbox"],props:{numArgs:1,argTypes:["hbox"],allowedInText:!0},handler:function(t,e){return{type:"enclose",mode:t.parser.mode,label:"\\fbox",body:e[0]}}}),Qt({type:"enclose",names:["\\cancel","\\bcancel","\\xcancel","\\sout"],props:{numArgs:1},handler:function(t,e,r){var a=t.parser,n=t.funcName,i=e[0];return{type:"enclose",mode:a.mode,label:n,body:i}},htmlBuilder:ur,mathmlBuilder:pr});var dr={};function fr(t){for(var e=t.type,r=t.names,a=t.props,n=t.handler,i=t.htmlBuilder,o=t.mathmlBuilder,s={type:e,numArgs:a.numArgs||0,greediness:1,allowedInText:!1,numOptionalArgs:0,handler:n},h=0;h0&&(b+=.25),l.push({pos:b,isDashed:t[e]})}for(y(i[0]),r=0;r0&&(M<(B+=v)&&(M=B),B=0),t.addJot&&(M+=f),z.height=S,z.depth=M,b+=S,z.pos=b,b+=M+B,h[r]=z,y(i[r+1])}var C,q,N=b/2+e.fontMetrics().axisHeight,O=t.cols||[],I=[];for(a=0,q=0;a=s)){var H=void 0;(a>0||t.hskipBeforeAndAfter)&&0!==(H=c.deflt(R.pregap,p))&&((C=Dt.makeSpan(["arraycolsep"],[])).style.width=H+"em",I.push(C));var D=[];for(r=0;r0){for(var G=Dt.makeLineSpan("hline",e,m),Y=Dt.makeLineSpan("hdashline",e,m),_=[{type:"elem",elem:h,shift:0}];l.length>0;){var W=l.pop(),X=W.pos-N;W.isDashed?_.push({type:"elem",elem:Y,shift:X}):_.push({type:"elem",elem:G,shift:X})}h=Dt.makeVList({positionType:"individualShift",children:_},e)}return Dt.makeSpan(["mord"],[h],e)},yr={c:"center ",l:"left ",r:"right "},wr=function(t,e){var r=new ve.MathNode("mtable",t.body.map((function(t){return new ve.MathNode("mtr",t.map((function(t){return new ve.MathNode("mtd",[Me(t,e)])})))}))),a=.5===t.arraystretch?.1:.16+t.arraystretch-1+(t.addJot?.09:0);r.setAttribute("rowspacing",a+"em");var n="",i="";if(t.cols){var o=t.cols,s="",h=!1,l=0,m=o.length;"separator"===o[0].type&&(n+="top ",l=1),"separator"===o[o.length-1].type&&(n+="bottom ",m-=1);for(var c=l;c0?"left ":"",n+=g[g.length-1].length>0?"right ":"";for(var x=1;x0&&c&&(d=1),a[u]={type:"align",align:p,pregap:d,postgap:0}}return n.colSeparationType=c?"align":"alignat",n};fr({type:"array",names:["array","darray"],props:{numArgs:1},handler:function(t,e){var r={cols:(Yt(e[0])?[e[0]]:Ft(e[0],"ordgroup").body).map((function(t){var e=Gt(t).text;if(-1!=="lcr".indexOf(e))return{type:"align",align:e};if("|"===e)return{type:"separator",separator:"|"};if(":"===e)return{type:"separator",separator:":"};throw new o("Unknown column alignment: "+e,t)})),hskipBeforeAndAfter:!0};return xr(t.parser,r,vr(t.envName))},htmlBuilder:br,mathmlBuilder:wr}),fr({type:"array",names:["matrix","pmatrix","bmatrix","Bmatrix","vmatrix","Vmatrix"],props:{numArgs:0},handler:function(t){var e={matrix:null,pmatrix:["(",")"],bmatrix:["[","]"],Bmatrix:["\\{","\\}"],vmatrix:["|","|"],Vmatrix:["\\Vert","\\Vert"]}[t.envName],r=xr(t.parser,{hskipBeforeAndAfter:!1},vr(t.envName));return e?{type:"leftright",mode:t.mode,body:[r],left:e[0],right:e[1],rightColor:void 0}:r},htmlBuilder:br,mathmlBuilder:wr}),fr({type:"array",names:["smallmatrix"],props:{numArgs:0},handler:function(t){var e=xr(t.parser,{arraystretch:.5},"script");return e.colSeparationType="small",e},htmlBuilder:br,mathmlBuilder:wr}),fr({type:"array",names:["subarray"],props:{numArgs:1},handler:function(t,e){var r=(Yt(e[0])?[e[0]]:Ft(e[0],"ordgroup").body).map((function(t){var e=Gt(t).text;if(-1!=="lc".indexOf(e))return{type:"align",align:e};throw new o("Unknown column alignment: "+e,t)}));if(r.length>1)throw new o("{subarray} can contain only one column");var a={cols:r,hskipBeforeAndAfter:!1,arraystretch:.5};if((a=xr(t.parser,a,"script")).body[0].length>1)throw new o("{subarray} can contain only one column");return a},htmlBuilder:br,mathmlBuilder:wr}),fr({type:"array",names:["cases","dcases"],props:{numArgs:0},handler:function(t){var e=xr(t.parser,{arraystretch:1.2,cols:[{type:"align",align:"l",pregap:0,postgap:1},{type:"align",align:"l",pregap:0,postgap:0}]},vr(t.envName));return{type:"leftright",mode:t.mode,body:[e],left:"\\{",right:".",rightColor:void 0}},htmlBuilder:br,mathmlBuilder:wr}),fr({type:"array",names:["aligned"],props:{numArgs:0},handler:kr,htmlBuilder:br,mathmlBuilder:wr}),fr({type:"array",names:["gathered"],props:{numArgs:0},handler:function(t){return xr(t.parser,{cols:[{type:"align",align:"c"}],addJot:!0},"display")},htmlBuilder:br,mathmlBuilder:wr}),fr({type:"array",names:["alignedat"],props:{numArgs:1},handler:kr,htmlBuilder:br,mathmlBuilder:wr}),Qt({type:"text",names:["\\hline","\\hdashline"],props:{numArgs:0,allowedInText:!0,allowedInMath:!0},handler:function(t,e){throw new o(t.funcName+" valid only within array environment")}});var Sr=dr;Qt({type:"environment",names:["\\begin","\\end"],props:{numArgs:1,argTypes:["text"]},handler:function(t,e){var r=t.parser,a=t.funcName,n=e[0];if("ordgroup"!==n.type)throw new o("Invalid environment name",n);for(var i="",s=0;s=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 来组织和构建项目。在这里,我们将使用一个基本的模板项目作为起点。以下是示例模板的文件夹结构: ![Figure 1.1: Folder structure of a sample template ](img/C14508_01_01.jpg) ###### 图 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 ``` 前一个命令的输出类似于以下内容: ![Figure 1.2: Checking out the sample project from GitHub ](img/C14508_01_02.jpg) ###### 图 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 ``` 前面命令的输出如下: ![Figure 1.3: Generating the Ninja build file ](img/C14508_01_03.jpg) ###### 图 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 ``` 前面的命令将在终端中显示以下输出: ![Figure 1.4: Files in the build folder ](img/C14508_01_04.jpg) ###### 图 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 ``` 您应该会看到如下所示的最终输出: ![Figure 1.5: Building with ninja ](img/C14508_01_05.jpg) ###### 图 1.5:用忍者建造 7. Type `ls` in the **build** folder and check whether we have generated the `CxxTemplate` executable or not: ```cpp ls ``` 前面的命令在终端中产生以下输出: ![Figure 1.6: Files in the build folder after running ninja ](img/C14508_01_06.jpg) ###### 图 1.6:运行 ninja 后构建文件夹中的文件 在上图中,可以看到生成了`CxxTemplate`可执行文件。 8. In the terminal, type the following command to run the `CxxTemplate` executable: ```cpp ./CxxTemplate ``` 终端中的前一个命令将提供以下输出: ![](img/C14508_01_07.jpg) ###### 图 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: ![Figure 1.8: New Project dialog box ](img/C14508_01_08.jpg) ###### 图 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: ![Figure 1.9: C++ Project dialog box ](img/C14508_01_09.jpg) ###### 图 1.9: C++ 项目对话框 4. 接受一切,包括切换到 C/C++ 视角,点击**完成**。 5. Click on the **Restore** button at the top-left corner to view the newly created project: ![Figure 1.10: The Restore button ](img/C14508_01_10.jpg) ###### 图 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: ![Figure 1.11: Project properties ](img/C14508_01_11.jpg) ###### 图 1.11:项目属性 7. Then, choose the **Project** | **Build All** menu item to build the project: ![Figure 1.12: Building the project ](img/C14508_01_12.jpg) ###### 图 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: ![Figure 1.13: The build output ](img/C14508_01_13.jpg) ###### 图 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: ![Figure 1.14: Running a project ](img/C14508_01_14.jpg) ###### 图 1.14:运行项目 10. 当它运行时,您将在**控制台**窗格中看到程序的输出,如下所示: ![Figure 1.15: Output of the project ](img/C14508_01_15.jpg) ###### 图 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: ![Figure 1.16: Creating a new class ](img/C14508_01_16.jpg) ###### 图 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 通过一条**未找到成员声明**消息来警告我们: ![Figure 1.17: Analyzer warning ](img/C14508_01_17.jpg) ###### 图 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. 使用**运行** | **运行**菜单选项运行项目。您应该会在终端中看到以下输出: ![Figure 1.18: Program output ](img/C14508_01_18.jpg) ###### 图 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 中构建和运行项目。 预期输出应该类似于以下内容: ![Figure 1.19: Final output ](img/C14508_01_19.jpg) ###### 图 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**: ![Figure 1.20: Build operation and its output ](img/C14508_01_20.jpg) ###### 图 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: ![Figure 1.21: Viewing the correct console output ](img/C14508_01_21.jpg) ###### 图 1.21:查看正确的控制台输出 ![Figure 1.22: Viewing the correct console output ](img/C14508_01_22.jpg) ###### 图 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 ``` 上述代码在终端中生成以下输出: ![Figure 1.23: Running the tests executable ](img/C14508_01_23.jpg) ###### 图 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 ../../.. ``` 这是您将看到的输出: ![Figure 1.24: Running ctest ](img/C14508_01_24.jpg) ###### 图 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: ![Figure 1.25: Changing the name of the run configuration ](img/C14508_01_25.jpg) ###### 图 1.25:更改运行配置的名称 8. Under **C/C++ Application**, select the **Search Project** option: ![Figure 1.26: Run Configurations ](img/C14508_01_26.jpg) ###### 图 1.26:运行配置 9. Choose **tests** in the new dialog: ![Figure 1.27: Creating the test run configuration and selecting the tests executable ](img/C14508_01_27.jpg) ###### 图 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: ![Figure 1.28: Run Configurations ](img/C14508_01_28.jpg) ###### 图 1.28:运行配置 11. 在接下来的运行中,您可以点击工具栏中播放按钮旁边的下拉菜单,或者选择**运行** | **运行历史**选择**扩展测试**: ![Figure 1.29: Finalizing the run configuration settings and selecting a configuration to run ](img/C14508_01_29.jpg) ###### 图 1.29:最终确定运行配置设置并选择要运行的配置 结果将类似于下面的截图: ![Figure 1.30: Run results of the unit test ](img/C14508_01_30.jpg) ###### 图 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: ![Figure 1.31: Output after running the test ](img/C14508_01_31.jpg) ###### 图 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: ![Figure 1.32: The build report ](img/C14508_01_32.jpg) ###### 图 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. 现在运行它,并在报告中观察所有测试是否通过: ![Figure 1.33: Test execution is successful ](img/C14508_01_33.jpg) ###### 图 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. 验证测试是否通过。 最终测试结果应该类似于以下内容: ![Figure 1.34: Final test results ](img/C14508_01_34.jpg) ###### 图 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 ``` 您将在终端中获得构建过程的详细输出,这可能看起来有点拥挤: ![Figure 1.35: The build process part 1 ](img/C14508_01_35.jpg) ###### 图 1.35:构建过程第 1 部分 ![Figure 1.36: The build process part 2 ](img/C14508_01_36.jpg) ###### 图 1.36:构建过程第 2 部分 ![Figure 1.37: The full build output ](img/C14508_01_37.jpg) ###### 图 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++ ``` 您将看到以下输出: ![Figure 1.38: The chain of symbolic links for /usr/bin/c++ ](img/C14508_01_38.jpg) ###### 图 1.38:用于/usr/bin/c++ 的符号链接链 因此,我们将在整个讨论中交替使用`c++ `和`g++ `。在我们前面引用的构建输出中,前四行是编译每个`。cpp`源文件并创建相应的`。o`对象文件。最后一行是将这些目标文件链接在一起,创建`CxxTemplate`可执行文件。下图直观地展示了这一过程: ![Figure 1.39: Execution stages of a C++ project ](img/C14508_01_39.jpg) ###### 图 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 ``` 我们看到如下预期的输出。这些是我们的目标文件: ![Figure 1.40: Compiled object files ](img/C14508_01_40.jpg) ###### 图 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`文件: ![Figure 1.41: Linked executable file ](img/C14508_01_41.jpg) ###### 图 1.41:链接的可执行文件 10. Now, run our executable by typing the following command: ```cpp ./CxxTemplate ``` 看看我们之前的输出: ![Figure 1.42: Executable file output ](img/C14508_01_42.jpg) ###### 图 1.42:可执行文件输出 现在,您已经检查了构建过程的细节并自己重新创建了它们,在下一节中,让我们来探索链接过程。 ### 连接步骤 在这一节中,让我们来看看两个源文件之间的连接,以及它们如何在同一个可执行文件中结束。请看下图中的**求和**功能: ![Figure 1.43: The linking process ](img/C14508_01_43.jpg) ###### 图 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); } ``` 我们有两个功能;一个在呼叫另一个。下面是编译后的输出: ![Figure 1.44: The compiled code ](img/C14508_01_44.jpg) ###### 图 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: ![Figure 1.45: Compiled code without demangling ](img/C14508_01_45.jpg) ###### 图 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(); } ``` 以下是这些新增行的编译版本: ![Figure 1.46: The compiled version ](img/C14508_01_46.jpg) ###### 图 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: ![Figure 1.47: Eclipse debug screen ](img/C14508_01_47.jpg) ###### 图 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: ![Figure 1.48: Stepping over the code ](img/C14508_01_48.jpg) ###### 图 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: ![Figure 1.49: Variable states through time ](img/C14508_01_49.jpg) ###### 图 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: ![Figure 1.50: View option of the debugger ](img/C14508_01_50.jpg) ###### 图 1.50:调试器的视图选项 此外,**表达式**视图可以帮助您计算从您浏览的值中不清楚的东西。 10. Click on **Expression** on the right-hand side and click on the **Add** button: ![Figure 1.51: Adding an expression ](img/C14508_01_51.jpg) ###### 图 1.51:添加表达式 11. Type **t+i** and hit *Enter*. Now you see the total in the list of expressions: ![Figure 1.52: Expression view with a new expression ](img/C14508_01_52.jpg) ###### 图 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: ![Figure 1.53: Working with breakpoints ](img/C14508_01_53.jpg) ###### 图 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: ![Figure 1.54: Working with the debugger ](img/C14508_01_54.jpg) ###### 图 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: ![Figure 1.55: Working with the debugger – the Step Over option ](img/C14508_01_55.jpg) ###### 图 1.55:使用调试器–单步执行选项 16. 突出显示的是下一行,如果我们再次单步执行,它将被执行。相反,请按“进入”按钮。这将带我们进入构造函数调用: ![Figure 1.56: Working with the debugger – the Step Into option ](img/C14508_01_56.jpg) ###### 图 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 ``` 如果这些是在函数的范围内声明的,那么编译器会从所谓的堆栈中为它们分配内存。这方面的内存布局可能如下所示: ![Figure 2A.1: Memory layout of variables](img/C14583_02A_01.jpg) ###### 图 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]**: ![Figure 2A.2: CMake console output](img/C14583_02A_02.jpg) ###### 图 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: ![Figure 2A.3: Selecting a different console](img/C14583_02A_03.jpg) ###### 图 2A.3:选择不同的控制台 这将显示构建的结果—它应该显示 0 个错误和 3 个警告: ![Figure 2A.4: Build process console output](img/C14583_02A_04.jpg) ###### 图 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**: ![Figure 2A.5: Launch Configuration menu](img/C14583_02A_05.jpg) ###### 图 2A.5:启动配置菜单 * 点击**新启动配置…** 。保持默认值不变,点击**下一步>T3。*** Change **Name** to **Exercise1** and then click **Search Project**: ![Figure 2A.6: Exercise1 Launch Configuration ](img/C14583_02A_06.jpg) ###### 图 2A.6:练习 1 启动配置 * 从二进制文件窗口显示的程序列表中,单击**练习 1** 并单击**确定**。* Click **Finish**. This will result in exercise1 being displayed in the Launch Configuration drop-down box: ![Figure 2A.7: Change to Launch Configuration](img/C14583_02A_07.jpg) ###### 图 2A.7:更改启动配置 * To run **Exercise1**, click on the **Run** button. Exercise1 will execute and display its output in the console: ![Figure 2A.8: Output from exercise1](img/C14583_02A_08.jpg) ###### 图 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: ![Figure 2A.9: Errors in Workspace dialog](img/C14583_02A_09.jpg) ###### 图 2A.9:工作空间对话框中的错误 * Click on **Cancel** to close the dialog. If **CDT Build Console [Lesson2A]** is not displayed, then select it as the active console: ![Figure 2A.10: Duplicate declaration error](img/C14583_02A_10.jpg) ###### 图 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**: ![Figure 2A.11: Auto declaration error – no initialization](img/C14583_02A_11.jpg) ###### 图 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**: ![Figure 2A.12: Assignment of an incorrect value type to a variable](img/C14583_02A_12.jpg) ###### 图 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**: ![Figure 2A.13: Assignment of an incorrect value type to an auto variable](img/C14583_02A_13.jpg) ###### 图 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**: ![Figure 2A.14: Assignment of a value that's too large to fit in the variable](img/C14583_02A_14.jpg) ###### 图 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`,没有任何争议: ![Figure 2A.15: Declaration of a function that takes no arguments and returns nothing](img/C14583_02A_15.jpg) ###### 图 2A.15:声明一个不接受参数也不返回任何内容的函数 第二个函数返回一个`int`值,名称为`main`,并接受两个参数。这些参数分别是`argc`和`argv`,并且使`int`和*指针分别指向* `char`类型的指针: ![Figure 2A.16: Declaration of a function that takes two arguments and returns an int](img/C14583_02A_16.jpg) ###### 图 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: ![Figure 2A.17: Output from the exercise2 program](img/C14583_02A_17.jpg) ###### 图 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: ![Figure 2A.18: Build failure due to undefined function](img/C14583_02A_18.jpg) ###### 图 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: ![Figure 2A.19: Memory layout for pointer variables](img/C14583_02A_19.jpg) ###### 图 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: ![Figure 2A.25: Exercise 4 output](img/C14583_02A_25.jpg) ###### 图 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**: ![Figure 2A.26: Cannot convert error](img/C14583_02A_26.jpg) ###### 图 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**: ![Figure 2A.27: Multiple errors for scoped enum changes](img/C14583_02A_27.jpg) ###### 图 2A.27:作用域枚举更改的多个错误 10. Locate the first error listed for this build: ![Figure 2A.28: First scoped enum error ](img/C14583_02A_28.jpg) ###### 图 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: ![Figure 2A.29: Second scoped enum error](img/C14583_02A_29.jpg) ###### 图 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: ![](img/C14583_02A_30.jpg) ###### 图 2A.30:第三范围枚举错误 这个错误报告(最后)你不能比较苹果和橘子。在这一点上,我们认为程序试图做一些没有意义的事情,没有必要试图修复其余的事情。我们可以通过再次将它转换为 int 来修复这个错误,但是我们也需要为下一个错误进行转换。最后一个错误是巴伦西亚缺少`Orange::`范围说明符。 15. 留给你一个练习,让文件以`橙色`作为范围枚举再次编译。 在本练习中,我们发现范围枚举改进了 C++ 的强类型检查,如果我们希望将它们用作整数值,那么我们需要强制转换它们,这与隐式转换的非范围枚举不同。 #### 编译器错误疑难解答 从前面的练习中可以看出,编译器可以从一个错误中生成大量的错误和警告消息。这就是为什么建议先找到第一个错误并先修复它。在 IDEs 中开发或使用带有颜色代码错误的构建系统可以使这变得更容易。 ### 结构和类别 枚举是用户定义类型中的第一种,但它们并没有真正扩展语言,以便我们能够在适当的抽象级别上表达问题的解决方案。然而,结构和类允许我们捕获和分组数据,然后关联方法以一致和有意义的方式操作数据。 如果我们考虑两个矩阵的乘法, *A (m x n)* 和 *B (n x p)* ,从而得到矩阵 *C (m x p)* ,那么 C 的第 I 行和第 jth 列的等式如下: ![](img/C14583_02A_31.jpg) ###### 图 2A.31:第 1 行和第 2 列的方程 如果我们每次想要乘两个矩阵时都要写它,我们最终会得到许多嵌套的 for 循环。但是如果我们可以把一个矩阵抽象成一个类,那么我们可以把它简单地表达为两个整数或两个浮点数的乘积: ```cpp Matrix a; Matrix b; // Code to initialize the matrices auto c = a * b; ``` 这就是面向对象设计的美妙之处——数据封装和概念的抽象被解释得如此之深,以至于我们可以很容易地理解程序试图实现什么,而不会被细节所掩盖。一旦我们确定矩阵乘法被正确实现,那么我们就可以自由地专注于在更高的层次上解决我们的问题。 下面的讨论涉及到类,但它同样适用于结构,并且主要适用于联合。在我们学习如何定义和使用类之后,我们将概述类、结构和联合之间的区别。 ### 分数等级 为了向您展示如何定义和使用类,我们将开发`分数`类来实现有理数。定义后,我们可以像使用任何其他内置类型(加、减、乘、除)一样使用`分数`,而不用担心细节——这是抽象。我们现在可以在更高的层次上思考和推理一个分数,也就是抽象的层次。 `分数`类将执行以下操作: * 包含两个整数成员变量,`m _ 分子`和`m _ 分母` * 提供复制自身、分配给自身、乘法、除法、加法和减法的方法 * 提供写入输出流的方法 为了实现上述目标,我们有以下定义: ![Figure 2A.32: Definitions of operations](img/C14583_02A_32.jpg) ###### 图 2A.32:操作的定义 此外,我们执行的操作将需要通过将其减少到最低项来标准化分数。为此,分子和分母都要除以它们的最大公约数(GCD)。 ### 构造函数、初始化和析构函数 用 C++ 代码表示的类定义是用于在内存中创建对象和通过对象的方法操作对象的模式。我们需要做的第一件事是告诉编译器我们希望声明一个新的类型——类。要声明`分数`类,我们从以下内容开始: ```cpp class Fraction { }; ``` 我们将它放在头文件 **Fraction.h** 中,因为我们希望在代码的其他区域重用这个类规范。 接下来我们需要做的是引入要存储在类中的数据,在这种情况下是`m _ 分子`和`m _ 分母`。它们都是 int 类型的: ```cpp class Fraction {   int m_numerator;   int m_denominator; }; ``` 我们现在已经声明了要存储的数据,并给它们起了一个名字,熟悉数学的人都会理解每个成员变量存储了什么: ![](img/C14583_02A_33.jpg) ###### 图 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); ``` 显式关键字也可以应用于其他运算符,编译器可以将其用于类型转换。 ### 类特殊成员函数–编译器生成规则 首先,如果我们声明任何其他形式的构造函数——默认、复制、移动或用户定义,将不会生成`默认构造函数`。其他特殊成员函数都不会影响其生成。 其次,声明析构函数就不会产生`析构函数`。其他特殊成员函数都不会影响其生成。 其他四个特殊函数的生成取决于析构函数或其他特殊函数之一的声明,如下表所示: ![](img/C14583_02A_34.jpg) ###### 图 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: ![](img/C14583_02A_35.jpg) ###### 图 2A.35:练习 5 典型输出 为分数报告的值来自不以任何方式初始化成员变量。如果你再运行一次,你很可能会得到不同的分数。 5. 点击几次**运行**按钮。你会看到分数发生了变化。 6. 在编辑器中,将构造函数更改如下: ```cpp Fraction() : m_numerator{0}, m_denominator{1} { } ``` 7. Click on the **Run** button and observe the output: ![](img/C14583_02A_36.jpg) ###### 图 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: ![](img/C14583_02A_37.jpg) ###### 图 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: ![Figure 2A.38: Exercise 6 output](img/C14583_02A_38.jpg) ###### 图 2A.38:练习 6 的输出 通过检查代码并将其与输出进行比较,我们会发现`值`变量允许我们操作(读取和写入)存储在`值`变量中的数据。我们有一个参考,`值`到`值`变量。我们还可以看到存储在`a`和`b`变量中的值是通过`swap()`函数交换的。 5. 在编辑器中,更改 swap 的函数定义: ```cpp void swap(const int& lhs, const int& rhs) ``` 6. 点击**运行**按钮。出现“工作空间中的错误”对话框时,单击**取消**。编译器报告的第一个错误如下所示: ![Figure 2A.39: Read-only error on assignment](img/C14583_02A_39.jpg) ###### 图 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: ![Figure 2A.40: Failing tests – multiplication](img/C14583_02A_40.jpg) ###### 图 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: ![Figure 2A.41: Passing tests](img/C14583_02A_41.jpg) ###### 图 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`: ![Figure 2A.42: Additional tests to fail](img/C14583_02A_42.jpg) ###### 图 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** 并点击**运行**按钮。这将产生以下输出: ![Figure 2A.43: Functional Fraction class](img/C14583_02A_43.jpg) ###### 图 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)`时,它需要计算出调用哪个先前定义的函数,因此它执行以下过程(非常简化): ![Figure 2A.44: Function overload resolution (simplified)](img/C14583_02A_44.jpg) ###### 图 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 轴)及其单元测试。 执行上述步骤后,预期输出如下: ![](img/C14583_02A_45.jpg) ###### 图 2A.45:成功运行活动程序 就本次活动而言,我们不会担心指数超出范围的可能性。我们将在*第 3 章*、*能够和应该之间的距离——对象、指针和继承*中讨论这一点。单位矩阵是一个正方形矩阵(在我们的例子中是 4 x 4),其中对角线上的所有值都设置为 1,其他所有值都为零。 当使用三维图形时,我们为点(顶点)和变换使用增广矩阵,以便所有的变换(平移、缩放、旋转)都可以通过使用乘法来实现。 一个`n × m`矩阵是一个由 n 行 m 个数组成的数组。例如,一个`2 x 3`矩阵可能如下所示: ![Figure 2A.46: Matrix of 2x3](img/C14583_02A_46.jpg) ###### 图 2A . 46:2x 3 的矩阵 一个三维顶点可以表示为一个`三元组(x,y,z)`。然而,我们用另一个纵坐标`w (=1 代表一个顶点,=0 代表一个方向)`来扩充它,使它成为一个`四元组(x,y,z,1)`。我们不使用元组,而是将其放在`4 x 1`矩阵中,如下所示: ![Figure 2A.47: 4x1 Matrix](img/C14583_02A_47.jpg) ###### 图 2A.47: 4x1 矩阵 如果我们将`4×1`矩阵(点)乘以一个`4×4`矩阵(变换),我们就可以操纵该点。如果`Ti`代表一个变换,那么我们可以将这些变换相乘来实现对点的一些操作: ![Figure 2A.48: Multiplying transformations](img/C14583_02A_48.jpg) ###### 图 2A.48:乘法变换 要乘以一个变换矩阵,`A x P = B`,我们执行以下操作: ![Figure 2A.49: Multiplying transformation matrix](img/C14583_02A_49.jpg) ###### 图 2A.49:乘法变换矩阵 我们也可以这样表达: ![Figure 2A.50: Expression of multiplying transformations](img/C14583_02A_50.jpg) ###### 图 2A.50:乘法变换的表达式 同样,两个`4×4`矩阵相乘也可以得到相同的结果,`AxB=C`: ![Figure 2A.51 Expression of 4x4 matrix multiplication:](img/C14583_02A_51.jpg) ###### 图 2A . 51 4x 4 矩阵乘法的表达式: 转换的矩阵如下: ![Figure 2A.52: List of matrices for transformation](img/C14583_02A_52.jpg) ###### 图 2A.52:用于转换的矩阵列表 #### 注意 这项活动的解决方案可以在第 635 页找到。 ## 总结 在这一章中,我们学习了 C++ 中的类型。首先,我们接触了内置类型,然后学习了如何创建我们自己的行为类似于内置类型的类型。我们学习了如何声明和初始化变量,了解了编译器从源代码中生成什么,将变量放在哪里,链接器如何将变量放在一起,以及在计算机内存中是什么样子。我们学习了一些围绕零规则和五规则的 C++ 部落智慧。这些构成了 C++ 的构建模块。在下一章中,我们将研究用 C++ 模板创建函数和类,并进一步探索应用于模板的类型推导。******** ================================================ FILE: docs/adv-cpp/03.md ================================================ # 三、不允许鸭子——模板和推导(二) ## 学习目标 本章结束时,您将能够: * 使用继承和多态性开发自己的类,以获得更大的效果 * 实现一个别名,使您的代码更容易阅读 * 使用 SFINAE 和 constexpr 开发模板来简化代码 * 使用 STL 实现您自己的解决方案,以利用泛型编程 * 描述类型演绎的背景和基本规则 本章将向您展示如何通过继承、多态性和模板来定义和扩展您的类型。 ## 简介 在前一章中,我们学习了如何在单元测试的帮助下开发我们自己的类型(类),并使它们像内置类型一样工作。我们被介绍了函数重载、三/五法则和零法则。 在本章中,我们将学习如何进一步扩展类型系统。我们将学习如何使用模板创建函数和类,并重新访问函数重载,因为它受到模板使用的影响。我们将被介绍一项新技术 **SFINAE** ,并使用它来控制我们模板中包含在生成代码中的部分。 ## 继承、多态性和接口 到目前为止,在我们面向对象设计和 C++ 的旅程中,我们一直专注于抽象和数据封装。我们现在将注意力转向**遗传**和**多态性**。什么是继承?什么是多态性?我们为什么需要它?考虑以下三个对象: ![Figure 2B.1: Vehicle objects ](img/C14583_02B_01.jpg) ###### 图 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) {}; }; ``` 在这种情况下,车辆类被称为**基类**或**超类**,而摩托车类被称为**衍生类**或**子类**。我们可以用图形表示如下,箭头从派生类指向基类: ![Figure 2B.2: Vehicle class hierarchy ](img/C14583_02B_02.jpg) ###### 图 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; } ``` 如果我们编译并运行这个程序,我们会得到以下输出: ![Figure 2B.3: Vehicle program output ](img/C14583_02B_03.jpg) ###### 图 2B.3:车辆程序输出 上图截图中`车辆::StartEngine 1500 cc`后的行都与`摩托车`有关。但是驱动线仍然显示`车辆::驱动`而不是预期的`摩托车::驱动`。这是怎么回事?问题是我们没有告诉编译器`Vehicle`类中的`Drive`方法可以被派生类修改(或者覆盖)。我们需要在代码中做一个改变: ```cpp virtual void Drive() {   std::cout << "Vehicle::Drive\n"; }; ``` 通过在成员函数声明前添加`virtual`关键字,我们告诉编译器,派生类可以(但不必)重写或替换该函数。如果我们进行这种更改,然后编译并运行程序,我们会得到以下输出: ![Figure 2B.4: Vehicle program output with virtual methods ](img/C14583_02B_04.jpg) ###### 图 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。 ![Figure 2B.5: Accessibility of base class members in derived classes ](img/C14583_02B_05.jpg) ###### 图 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: ![Figure 2B.6: Exercise 1 default output ](img/C14583_02B_06.jpg) ###### 图 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: ![Figure 2B.7: Output of the modified program ](img/C14583_02B_07.jpg) ###### 图 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: ![Figure 2B.8: Output of the polymorphic version ](img/C14583_02B_08.jpg) ###### 图 2B.8:多态版本的输出 从前面的截图可以看到,**摧毁精灵**等的日志已经消失。问题是容器保存指向基类的指针,并且它不知道如何在每种情况下调用完整的析构函数。 11. 要解决这个问题,只需将`的析构函数`声明为虚拟的: ```cpp virtual ~ICharacter() { ``` 12. 点击**运行**按钮重建并运行练习。输出内容如下: ![Figure 2B.9: Output from the full polymorphic version ](img/C14583_02B_09.jpg) ###### 图 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)中的信息将所有引用的符号解析到导出的符号列表中,并用该信息修补代码段,以便它们可以正确运行。从图形上看,这描述如下: ![Figure 2B.10: Parts of the object files and the executable file ](img/C14583_02B_10.jpg) ###### 图 2B.10:部分目标文件和可执行文件 出于以下讨论的目的,基站和数据段将简称为数据段(唯一的区别是基站未初始化)。当一个程序被执行时,它被加载到内存中,并且它的内存看起来有点像可执行文件的布局——它包含文本段、数据段、BSS 段和主机系统分配的空闲内存,后者包含所谓的**栈**和**堆**。堆栈通常从内存顶部开始向下增长,而堆从 BSS 结束的地方开始向上增长,朝向堆栈: ![Figure 2B.11: CxxTemplate runtime memory map ](img/C14583_02B_11.jpg) ###### 图 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; } ``` 我们将获得以下预期输出: ![Figure 2B.12: Clamp program output ](img/C14583_02B_12.jpg) ###### 图 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: ![Figure 2B.13: Exercise 2 output ](img/C14583_02B_13.jpg) ###### 图 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()`的模板的专门化。当我们运行它时,会生成以下输出: ![Figure 2B.14: Erroneous print template program output ](img/C14583_02B_14.jpg) ###### 图 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)将生成以下错误: ![Figure 2B.15: Compiler error for the modified print template program ](img/C14583_02B_15.jpg) ###### 图 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)将生成以下错误: ![Figure 2B.16: Second compiler error for the modified print template program ](img/C14583_02B_16.jpg) ###### 图 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); } ``` 如果我们现在编译程序,它会完成编译并链接程序。如果我们运行它,它现在会产生以下(预期的)输出: ![Figure 2B.17: Corrected print template program output ](img/C14583_02B_17.jpg) ###### 图 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: ![Figure 2B.18: Exercise 3 specialized template output ](img/C14583_02B_18.jpg) ###### 图 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: ![Figure 2B.19: Exercise 3 general template only output ](img/C14583_02B_19.jpg) ###### 图 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: ![Figure 2B.20: stringify tailored for boolean ](img/C14583_02B_20.jpg) ###### 图 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: ![Figure 2B.21: constexpr if version template output ](img/C14583_02B_21.jpg) ###### 图 2B.21: constexpr if 版本模板输出 10. 如果将有多个模板的原始版本与最终版本进行比较,你会发现最终版本更像是一个正常的功能,更容易阅读和维护。 在练习中,我们了解了在 C++ 17 中使用新的 constexpr if 构造时,我们的模板可以变得多么简单和紧凑。 ### 函数重载再探 当我们第一次讨论函数重载时,我们只考虑了函数名来自手工编写的函数列表的场景。现在,我们需要更新这个。我们还可以编写具有相同名称的模板化函数。就像我们之前做的那样,当编译器遇到行`print(55)`时,它需要计算出要调用哪个先前定义的函数。因此,它执行以下过程(非常简单): ![Figure 2B.22: Function overload resolution with templates (simplified) ](img/C14583_02B_22.jpg) ###### 图 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; } ``` 编译器会抛出以下错误: ![Figure 2B.23: Compiler errors showing deduced types ](img/C14583_02B_23.jpg) ###### 图 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); } ``` 编译器会产生以下错误: ![Figure 2B.24: Compiler error showing a deduced type for the pass by type ](img/C14583_02B_24.jpg) ###### 图 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); } ``` 编译器将生成以下错误: ![Figure 2B.25: Compiler error showing the deduced type for pass by reference ](img/C14583_02B_25.jpg) ###### 图 2B.25:显示通过引用传递的推导类型的编译器错误 由此我们可以看出,编译器把 T 作为 **int** 从 ParamType 作为**int&T3。将 x 更改为常量 int 并不意外,因为从 ParamType 推导出 T 是**常量 int** 为**常量 int &** :** ![Figure 2B.26: Compiler error showing the deduced type for pass by const reference ](img/C14583_02B_26.jpg) ###### 图 2B.26:编译器错误显示了常量引用传递的推导类型 同样,像以前一样,引入 rx 作为常量 int 的引用,也不会让人感到意外,因为从 ParamType 推导出 T 是`常量 int`作为`常量 int &`: ```cpp void types() {     const int x = 42;     const int& rx = x;     function(rx); } ``` ![Figure 2B.27: Compiler error showing the deduced type when passing a const reference ](img/C14583_02B_27.jpg) ###### 图 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); ``` 编译器错误如下: ![Figure 2B.28: Compiler error showing the deduced type for the array argument when passed by reference ](img/C14583_02B_28.jpg) ###### 图 2B.28:编译器错误,显示了通过引用传递数组参数时推导出的类型 这一次,数组被捕获作为参考,大小也被包括在内。所以,如果 ary 被声明为`ary【10】`,那么将会产生一个完全不同的函数。让我们将模板还原为以下内容: ```cpp template void function(T parameter) {     TypeDisplay type; } ``` 如果我们试图编译数组调用,那么错误报告如下: ![Figure 2B.29: Compiler error showing the deduced type for the array argument when passed by value ](img/C14583_02B_29.jpg) ###### 图 2B.29:通过值传递数组参数时显示推导出的类型的编译器错误 我们可以看到,在这种情况下,当将数组传递给函数时,数组已经像通常的行为一样衰减了。我们在谈论*非类型模板参数*时看到了这种行为。 **情况 3: ParamType 是右值引用(T & & )** & T 被称为右值引用,而& T 被称为左值引用。C++ 不仅通过类型来表征表达式,还通过名为**值类别**的属性来表征表达式。这些类别控制编译器中的表达式计算,包括创建、复制和移动临时对象的规则。C++ 17 标准中定义了五个表达式值类别,它们具有以下关系: ![Figure 2B.30: C++ value categories ](img/C14583_02B_30.jpg) ###### 图 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; } ``` 当这个程序被编译和执行时,我们获得以下输出: ![Figure 2B.31: Output from the SFINAE expression ](img/C14583_02B_31.jpg) ###### 图 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: ![Figure 2B.32: Initial unit test for stacks ](img/C14583_02B_32.jpg) ###### 图 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: ![Figure 2B.33: Jumping to a Failing test ](img/C14583_02B_33.jpg) ###### 图 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 将运行并生成类似于以下输出的内容: ![Figure 2B.34: Exercise 4 output ](img/C14583_02B_34.jpg) ###### 图 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 方法或一般大小写。 执行上述步骤后,预期输出应该如下所示: ![Figure 2B.35: Output from the successful implementation of contains ](img/C14583_02B_35.jpg) ###### 图 2B.35:成功实现 contains 的 suc 输出 #### 注意 这项活动的解决方案可以在第 653 页找到。 ## 总结 在这一章中,我们学习了接口、继承和多态性,这些扩展了我们对类型的处理技巧。我们第一次尝试使用 C++ 模板进行泛型编程,并接触了该语言从 C++ 标准库中免费提供给我们的东西,其中包括 STL。我们探索了 C++ 的一个刚刚好用的特性,那就是模板类型推演,使用模板的时候让我们的生活变得更加轻松。然后,我们进一步学习了模板,并学习了如何使用 SFINAE 和 if constexpr 来控制编译器包含的模板部分。这些构成了我们进入 C++ 之旅的基石。在下一章中,我们将重新访问堆栈和堆,并了解什么是异常、发生了什么以及何时发生。我们还将学习如何在异常发生时保护我们的程序免受资源损失。 ================================================ FILE: docs/adv-cpp/04.md ================================================ # 四、不允许泄漏——异常和资源 ## 学习目标 本章结束时,您将能够: * 开发类来管理资源 * 开发异常健壮的代码,这样资源就不会通过 RAII 泄漏 * 实现可以通过移动语义转移资源所有权的类 * 实现控制隐式转换的类 在本章中,您将学习如何使用类来管理资源、防止泄漏以及防止复制大量数据。 ## 简介 在*章 2A* 、*不允许鸭子-类型、演绎*中,我们简单的接触了一些概念,比如智能指针和移动语义。在本章中,我们将进一步探讨它们。事实证明,这些主题与资源管理和编写健壮的代码(能够经常长时间运行而没有问题的代码)密切相关。 为了理解会发生什么,我们将探索变量在内存中的位置,以及当它们超出范围时会发生什么。 我们将研究编译器为我们输入的内容生成什么样的汇编代码,并且我们将探索当异常发生时所有这些是如何受到影响的。 ### 可变范围和寿命 在*章 2B* 、*不允许鸭子-模板和演绎*中,我们讨论了可变范围和生存期。让我们快速浏览一下它们的不同类型: **范围**: * **本地范围**(也称为**块范围**):这适用于在花括号(`{}`)内的块内声明的任何内容。 * **全局/文件范围**:这适用于在正常函数或类之外声明的变量,也适用于正常函数。 **寿命**: * **自动生存期**:这里,局部变量在声明时创建,在退出所在范围时销毁。这些由堆栈管理。 * **永久寿命**:这里,全局和静态局部变量都有永久寿命。 * **动态寿命**:这里,变量是在程序员的要求下创建和销毁的(使用`新增的`和`删除操作符`)。这些变量从堆中分配内存。 我们将使用以下程序来弄清楚`局部变量`的行为——那些具有`自动寿命`和那些具有`动态寿命`的变量: ![](img/C14583_03_01.jpg) ###### 图 3.1:可变范围和寿命的测试程序 当我们运行前面的程序时,会生成以下输出: ![Figure 3.2: Output from Lifetime test program](img/C14583_03_02.jpg) ###### 图 3.2:寿命测试程序的输出 前面输出中的十六进制数(`0xnnnnnn`)是正在构造或析构的 Int 对象的地址。我们的程序从进入`第 46 行`开始,使用`主()`功能。在这一点上,程序已经做了大量的初始化,以便我们可以随时使用所有的东西。下图是两个堆栈–**电脑堆栈**和**数据堆栈**。 这些都是帮助我们解释幕后发生的事情的抽象概念。`PC 栈` ( `程序计数器栈`)用于记住程序计数器的值(一个指向需要运行的下一条指令的寄存器),而`数据栈保存`我们正在处理的值或地址。虽然这是两个独立的堆栈,但在真正的 CPU 上,它很可能作为一个堆栈来实现。让我们看看下面的表格,其中我们使用了缩写`OLn`来表示来自前面程序输出的行号: ![Figure 3.3: Detailed analysis of the test program’s execution (part 1)](img/C14583_03_03.jpg) ###### 图 3.3:测试程序执行的详细分析(第 1 部分) 下面是测试程序执行的详细分析的第二部分: ![Figure 3.4: Detailed analysis of the test program’s execution (part 2)](img/C14583_03_04.jpg) ###### 图 3.4:测试程序执行的详细分析(第 2 部分) 下面是测试程序执行的详细分析的第三部分: ![Figure 3.5: Detailed analysis of the test program’s execution (part 3)](img/C14583_03_05.jpg) ###### 图 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()`函数中添加日志记录,这样我们就可以探索异常的行为: ![Figure 3.6: Modified test program for investigating exceptions](img/C14583_03_06.jpg) ###### 图 3.6:用于调查异常的修改后的测试程序 当我们编译并运行前面的程序时,会生成以下输出: ![Figure 3.7: Output from the test program](img/C14583_03_07.jpg) ###### 图 3.7:测试程序的输出 在前面的代码中,您可以看到注释被添加到右侧。现在,我们删除程序中`结果 2`行的注释,重新编译程序,然后重新运行它。生成的新输出如下所示: ![Figure 3.8: Output from the test program – result2](img/C14583_03_08.jpg) ###### 图 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; } ``` 当我们重新编译并运行程序时,我们会得到以下输出: ![Figure 3.9: Output when we throw an exception](img/C14583_03_09.jpg) ###### 图 3.9:当我们抛出异常时的输出 从前面的输出中我们可以看出,变化不大。只是我们没有得到一个`浮点异常`(核心被转储)——程序仍然终止,但是没有转储核心。然后我们在`main()`函数中添加了一个`try/catch`块,以确保异常不再被处理。 ![Figure 3.10: Catching the Exception ](img/C14583_03_10.jpg) ###### 图 3.10:捕捉异常 重新编译程序并运行它,以获得以下输出: ![Figure 3.11: Output from the program that catches the exception](img/C14583_03_11.jpg) ###### 图 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. ![Figure 3.12: Existing tests all pass (Runs: 18)](img/C14583_03_12.jpg) ###### 图 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: ![Figure 3.13: New failing test ThrowsDomainErrorForZeroDenominator](img/C14583_03_13.jpg) ###### 图 3.13:新的失败测试 9. 点击失败的测试名称–现在**消息**窗口将显示预期行为和实际行为。您可能需要向右滚动才能全部阅读。在最右边,它表示“`预期……抛出一个 std::domain_error 类型的异常`”,下一行表示“`实际:它什么都不抛出`”。 10. Double-click on the message and it will take you to the following test: ![Figure 3.14: The failing test](img/C14583_03_14.jpg) ###### 图 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: ![Figure 3.15: Another failing test](img/C14583_03_15.jpg) ###### 图 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`: ![Figure 3.16: pop Stack test fails](img/C14583_03_16.jpg) ###### 图 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++ 标准库将`标准::异常`定义为所有标准库异常的基类。该标准定义了以下第一级层次的`异常` / `错误`(括号中的数字表示有多少个异常源自该类): ![Figure 3.17: Standard Library exception Hierarchy (two levels)](img/C14583_03_17.jpg) ###### 图 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: ![Figure 3.18: Exercise 2 output – default handler caught the exception](img/C14583_03_18.jpg) ###### 图 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: ![Figure 3.19: std::exception handler has been used](img/C14583_03_19.jpg) ###### 图 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: ![Figure 3.20: Leaky memory and files from Exercise3.cpp](img/C14583_03_20.jpg) ###### 图 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: ![Figure 3.21: No file leaks](img/C14583_03_21.jpg) ###### 图 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–这一次,您应该看到监视器没有报告泄漏: ![Figure 3.22: No leaks – memory or files](img/C14583_03_22.jpg) ###### 图 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 } ``` 当我们运行这个程序时,会生成以下输出: ![Figure 3.23: Smart Pointer Program output](img/C14583_03_23.jpg) ###### 图 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; } ``` 重新运行程序后,我们得到以下输出: ![Figure 3.24: Smart pointer program output using move semantics](img/C14583_03_24.jpg) ###### 图 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; } ``` 执行时,它输出以下内容: ![Figure 3.25: Weak pointer program output](img/C14583_03_25.jpg) ###### 图 3.25:弱指针程序输出 这表明当应用退出时,面板和`文本窗口`没有被破坏。这是因为他们彼此都持有`共享 _ptr`,因此两者的参考计数不会归零并触发销毁。如果我们用图解法描述这个结构,那么我们可以看到它有一个`共享 _ptr`循环: ![Figure 3.26: weak_ptr and shared_ptr cycles](img/C14583_03_26.jpg) ###### 图 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; } ``` 当我们运行这个程序时,我们会收到以下输出: ![](img/C14583_03_27.jpg) ###### 图 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: ![Figure 3.28: Exercise 4 output](img/C14583_03_28.jpg) ###### 图 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: ![Figure 3.29: Compiler error for Step 5](img/C14583_03_29.jpg) ###### 图 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: ![Figure 3.30: Successful output for Exercise 4 (after EXERCISE4_STEP = 5)](img/C14583_03_30.jpg) ###### 图 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: ![Figure 3.31: Corrected printSharedPointer output](img/C14583_03_31.jpg) ###### 图 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: ![](img/C14583_03_32.jpg) ###### 图 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: ![Figure 3.33: Managing arrays in unique_ptr](img/C14583_03_33.jpg) ###### 图 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。 在执行了前面的步骤之后,预期的输出从一开始就不会改变: ![Figure 3.34: Activity 1 output after successful conversion to use RAII](img/C14583_03_34.jpg) ###### 图 3.34:成功转换为使用 RAII 后的活动 1 输出 #### 注意 这个活动的解决方案可以在第 657 页找到。 ### 什么时候调用函数? C++ 程序执行的所有操作本质上都是函数调用(尽管编译器可能会将它们优化为内联操作序列)。然而,由于**语法糖**,你正在进行函数调用可能并不明显。语法糖是编程语言中的语法,它使阅读或表达变得更容易。比如你写`a = 2 + 5`的时候,本质上是在调用`运算符=( & a,运算符+(2,5))`。只是这种语言允许我们编写第一种形式,但第二种形式允许我们重载运算符,并将这些功能扩展到用户定义的类型。 以下机制导致对函数的调用: * 对函数的显式调用。 * 所有运算符,如+、-、*、/、%等,以及 new/delete。 * 变量声明–如果存在初始化值,将导致用参数调用构造函数。 * 用户定义的文字–我们没有处理这些,但是本质上,我们为`类型运算符“【名称(参数)`”定义了一个重载。然后我们可以编写诸如 10_km 这样的东西,这使得我们的代码更容易理解,因为它携带了语义信息。 * 从一个值到另一个值的铸造(`静态 _ 铸造< >`,`const _ 铸造< >`,`重新解释 _ 铸造< >`,以及`动态 _ 铸造< >`)。同样,我们还有另一个运算符重载,它允许我们从一种类型转换为另一种类型。 * 在函数重载期间,可能需要将一种类型转换为另一种类型,以便它与函数原型相匹配。它可以通过调用具有正确参数类型的构造函数来创建临时的,或者通过隐式调用的强制转换操作符来实现。 编译器中的每一个结果都决定了一个函数必须被调用。在确定需要调用函数后,它必须找到与名称和参数匹配的函数。这是我们将在下一节讨论的内容。 ### 调用哪个函数 在*章节 2A**不允许鸭子-类型和演绎*中,我们看到功能过载解析执行如下: ![Figure 3.35: Function overload resolution](img/C14583_03_35.jpg) ###### 图 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"); } ``` 当我们编译并运行前面的程序时,我们得到了预期的输出: ![Figure 3.36: ADL Sample program output](img/C14583_03_36.jpg) ###### 图 3.36: ADL 示例程序输出 C++ 标准委员会随后决定引入一个`is_substring()`函数,如下所示: ```cpp namespace std { void is_substring(std::string superstring, const char* substring) {     std::cout << "std::is_substring()\n"; } } ``` 如果我们将它添加到文件的顶部,编译并重新运行它,我们现在会得到以下输出: ![Figure 3.37: ADL issue program output](img/C14583_03_37.jpg) ###### 图 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: ![Figure 3:38: Successful execution of Exercise 5](img/C14583_03_38.jpg) ###### 图 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: ![Figure 3.39: Compilation fails under C++ 17 – ambiguous function call](img/C14583_03_39.jpg) ###### 图 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";     } } ``` 当我们编译它并运行前面的程序时,我们会得到以下输出: ![Figure 3.40: Implicit conversion sample program output](img/C14583_03_40.jpg) ###### 图 3.40:隐式转换示例程序输出 嗯,这可能有点出乎意料,这编译并实际产生了一个输出。**实数**变量属于**实数**类型,它有一个要浮动的转换运算符–**运算符 float()** 。 **test()** 函数以一个 **bool** 作为参数,如果条件一定会导致一个 **bool** 。如果数值为零,编译器会将任何数值类型转换为值为 false 的**布尔**类型,如果数值不为零,则转换为 true。但是,如果这不是我们想要的行为,我们可以通过在函数声明前加上显式关键字来防止它。假设我们更改了行,它的内容如下: ```cpp explicit operator float() {return m_value;} ``` 如果我们现在试图编译它,我们会得到两个错误: ![Figure 3.41: Compile errors because implicit conversion was removed.](img/C14583_03_41.jpg) ###### 图 3.41:编译错误,因为隐式转换被移除了。 这两种情况都与无法将实数类型转换为布尔值有关–首先,在调用点进行`测试()`,然后在 if 条件下进行。 现在,让我们引入一个 bool 转换运算符来解决这个问题。 ```cpp operator bool() {return m_value == 0.0;} ``` 我们现在可以再次构建程序。我们将收到以下输出: ![Figure 3.42: Introducing the bool operator replaces implicit conversion](img/C14583_03_42.jpg) ###### 图 3.42:引入 bool 运算符代替隐式转换 `布尔`值现在为假,而以前为真。这是因为浮点转换返回的值的隐式转换不是零,然后转换为 true。 从 C++ 11 开始,所有的构造函数(复制和移动构造函数除外)都被认为是转换构造函数。这意味着,如果它们不是用显式声明的,那么它们可用于隐式转换。同样,任何未声明为显式的转换运算符都可以用于隐式转换。 `C++ 核心指南`有两条与隐式转换相关的规则: * **C.46** :默认情况下,将单参数构造函数声明为显式的 * **C.164** :避免隐式转换运算符 ### 上下文转换 如果我们现在对我们的小程序做一个进一步的改变,我们可以进入所谓的上下文转换。让我们明确 bool 运算符,并尝试编译程序: ```cpp explicit operator bool() {return m_value == 0.0;} ``` 我们将收到以下输出: ![Figure 3.43: Compile errors with explicit bool operator](img/C14583_03_43.jpg) ###### 图 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: ![Figure 3.44: Default output from Exercise 6](img/C14583_03_44.jpg) ###### 图 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: ![Figure 3.45: Failed conversion of int to Voltage](img/C14583_03_45.jpg) ###### 图 3.45:整数到电压的转换失败 6. 从构造函数中移除显式,并将`计算`函数改为引用: ```cpp void calculate(Voltage& v) ``` 7. Click on the **Run** button to recompile the code – now, we get the following error: ![](img/C14583_03_46.jpg) ###### 图 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: ![Figure 3.47: Failed conversion of Voltage to float](img/C14583_03_47.jpg) ###### 图 3.47:电压转换为浮动失败 10. 现在,将以下铸造操作员添加到`电压`等级: ```cpp operator float() const {     return m_emf; } ``` 11. Click on the **Run** button to recompile the code and run it: ![Figure 3.48: Successfully converted Voltage to float](img/C14583_03_48.jpg) ###### 图 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: ![Figure 3.49: Failure to convert Voltage to float](img/C14583_03_49.jpg) ###### 图 3.49:无法将电压转换为浮动 13. 通过将显式声明添加到强制转换中,我们可以防止编译器使用转换运算符。更改带有错误的行,将伏特变量转换为浮点数: ```cpp use_float(static_cast(volts)); ``` 14. 点击**运行**按钮重新编译代码并运行。 ![Figure 3.50: Conversion of Voltage into float with cast works again](img/C14583_03_50.jpg) ###### 图 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. 加上`减法`运算符,从两个`日期`的`差值`中返回一个`天数`值。 完成这些步骤后,您应该会收到以下输出: ![Figure 3.51: Output of a successful Date sample application](img/C14583_03_51.jpg) ###### 图 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`。然而,由于我们如何实现两者之间的乘法运算符,我们创建了编译时或**物理依赖关系**。 ![Figure 4.1: Physical Dependencies of Matrix3d and Point3d ](img/C14583_04_01.jpg) ###### 图 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; } ``` 执行时,上述程序产生以下输出: ![Figure 4.2: Sample Pimpl implementation output ](img/C14583_04_02.jpg) ###### 图 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 编译器),如下所示: ![Figure 4.3: Failed compilation of Pimpl using unique_ptr ](img/C14583_04_03.jpg) ###### 图 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; ``` 如果我们编译并运行我们的程序,那么将产生以下输出: ![Figure 4.4: Sample unique_ptr Pimpl implementation output ](img/C14583_04_04.jpg) ###### 图 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: ![Figure 4.5: Exercise 1 Program output ](img/C14583_04_05.jpg) ###### 图 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. 点击**运行**按钮重新构建程序。程序将再次运行以产生原始输出。 ![Figure 4.6: The Kitchen program output using Pimpl ](img/C14583_04_06.jpg) ###### 图 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; } ``` 该程序在编译和执行时会产生以下输出: ![Figure 4.7: Function Pointer Program output ](img/C14583_04_07.jpg) ###### 图 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; } ``` 当我们试图编译这个程序时,我们会得到以下错误: ![Figure 4.8: Errors compiling function pointer program ](img/C14583_04_08.jpg) ###### 图 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; } ``` 它成功编译并运行: ![Figure 4.9: Function pointer program using static member function ](img/C14583_04_09.jpg) ###### 图 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; } ``` 创建了两个类型为`缩放器`的对象,它们被用作生成输出的线内的函数。上述程序产生以下输出: ![Figure 4.10: Functors program output ](img/C14583_04_10.jpg) ###### 图 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 values{1,2,3,4,5};     PrintVector("Before transform", values);     std::transform(values.begin(), values.end(), values.begin(), Scaler(3));     PrintVector("After transform", values);     std::cout << "Complete.\n";     return 0; } ``` 如果我们运行这个程序,生成的输出将如下所示: ![Figure 4.11: Program output showing Scaler transformed vectors ](img/C14583_04_11.jpg) ###### 图 4.11:显示缩放器变换向量的程序输出 ### 练习 2:实现功能对象 在本练习中,我们将实现两个不同的函数对象,它们可以与 STL 算法一起工作。 1. 在 Eclipse 中打开**第 4 课**项目,然后在**项目浏览器**中,依次展开**第 4 课**和**练习 02** ,双击**练习 2.cpp** 将本练习的文件打开到编辑器中。 2. 由于这是一个基于 CMake 的项目,将当前的构建器更改为 CMake 构建(可移植)。 3. 点击**启动配置**下拉菜单,选择**新启动配置……**。将**l4 练习 2** 配置为以**练习 2** 的名称运行。 4. Click on the **Run** button. Exercise 2 will run and produce the following output: ![Figure 4.12: Exercise 2 Initial output ](img/C14583_04_12.jpg) ###### 图 4.12:练习 2 初始输出 我们要做的第一件事是通过引入一个函数对象来修复输出的格式。 5. 在编辑器中,`main()`函数的定义前添加以下类定义: ```cpp struct Printer {     void operator()(int n)     {         std::cout << m_sep << n;         m_sep = ", ";     }     const char* m_sep = ""; }; ``` 6. Inside the **main()** method replace the following code ```cpp std::cout << "Average of ["; for( auto n : values )     std::cout << n << ", "; std::cout << "] = "; ``` **同** ```cpp std::cout << "Average of ["; std::for_each(values.begin(), values.end(), Printer()); std::cout << "] = "; ``` 7. Click on the **Run** button. The exercise will run and produce the following output: ![Figure 4.13: Exercise 2 Improved output format ](img/C14583_04_13.jpg) ###### 图 4.13:练习 2 改进了输出格式 8. `打印机`类的内部状态允许我们修改格式。现在,介绍一个`聚合器`类,它允许我们计算`平均值`。在文件顶部添加以下类定义: ```cpp struct Averager {     void operator()(int n)     {         m_sum += n;         m_count++ ;     }     float operator()() const     {         return static_cast(m_sum)/(m_count==0?1:m_count);     }     int m_count{0};     int m_sum{0}; }; ``` 9. 修改`main()`方法使用`Averager`类如下: ```cpp int main(int argc, char**argv) {     std::cout << "\n------ Exercise 2 ------\n";     std::vector values {1,2,3,4,5,6,7,8,9,10};     Averager averager = std::for_each(values.begin(), values.end(),     Averager());     std::cout << "Average of [";     std::for_each(values.begin(), values.end(), Printer());     std::cout << "] = ";     std::cout << averager() << "\n";     std::cout << "Complete.\n";     return 0; } ``` 10. 点击**运行**按钮。该练习将运行并产生以下输出: ![Figure 4.14: Exercise 2 output with average ](img/C14583_04_14.jpg) ###### 图 4.14:练习 2 的平均输出 注意 **std::for_each()** 返回传递到其中的**平均器**的实例。该实例被复制到变量**平均器**中,然后包含计算平均值所需的数据。在本练习中,我们实现了两个函数对象或**函子**类:**平均器**和**打印机**,当传递到 STL 算法**时,我们可以使用它们作为函数。** ### 标准::功能< >模板 C++ 11 引入了一个通用的多态函数包装器模板,`std::function < >`,使得实现回调和其他函数相关功能变得更加容易。`std::function`保存一个被称为**目标**的可调用对象。如果不包含目标,则称为**空**。调用空的`std::function`会导致`std::bad_function_call`异常被抛出。 函数对象可以存储、复制或调用以下任何可调用对象的目标:函数、函数对象(定义`运算符()`)、成员函数指针或 lambda 表达式。我们将在*主题中讨论更多关于λ表达式的内容。* 当实例化一个`std::function`对象时,只需要提供函数签名,而不需要提供用来初始化它的值,导致一个空实例。实例化如下进行: ![Figure 4.15: Structure of a std::function declaration ](img/C14583_04_15.jpg) ###### 图 4.15:标准::函数声明的结构 模板的参数,定义由`变量`存储的目标的`函数签名`。签名以返回类型(可能是 void)开始,然后在括号内放置函数将被调用的类型列表。 自由函数和带`标准::函数`的`函子`的使用是直接的。假设签名与传递给`标准::函数`模板的参数匹配,我们可以简单地将自由函数或`函子`等同于实例。 ```cpp void FreeFunc(int value); struct Functor {     void operator()(int value); }; std::function func; Functor functor; func = FreeFunc;                     // Set target as FreeFunc func(32);                            // Call FreeFunc with argument 32 func = functor;                      // set target as functor func(42);                            // Call Functor::operator() with argument 42 ``` 然而,如果我们想在一个对象实例上使用一个方法,那么我们需要使用另一个 STL 助手模板`std::bind()`。如果我们运行以下程序: ```cpp #include #include struct Binder {     void method(int a, int b)     {         std::cout << "Binder::method(" << a << ", " << b << ")\n";     } }; int main() {     std::cout << "\n\n------ Member Functions using bind ------\n";     Binder binder;     std::function func;     auto func1 = std::bind(&Binder::method, &binder, 1, 2);     auto func2 = std::bind(&Binder::method, &binder, std::placeholders::_1, std::placeholders::_2);     auto func3 = std::bind(&Binder::method, &binder, std::placeholders::_2, std::placeholders::_1);     func = func1;     func(34,56);     func = func2;     func(34,56);     func = func3;     func(34,56);     std::cout << "Complete.\n";     return 0; } ``` 然后我们得到以下输出: ![Figure 4.16: Program output using std::bind() and std::function ](img/C14583_04_16.jpg) ###### 图 4.16:使用标准::bind()和标准::函数的程序输出 需要注意的几点: * 使用类作为范围说明符引用函数`方法()`; * `Binder`实例的地址作为第二个参数传递给`std::bind()`,这使其成为传递给`方法()`的第一个参数。这是必要的,因为所有非静态成员都有一个隐式的`这个`指针作为第一个参数传递。 * 使用`标准::占位符`定义,我们可以绑定调用绑定方法时使用的参数,甚至可以更改传递的顺序(如`函数 3`所示)。 C++ 11 引入了一些被称为 lambda 表达式的语法糖,使得定义匿名函数变得更加容易,这些匿名函数也可以用来绑定方法并将其分配给`STD::function`instance 表达式。我们将在*主题中讨论更多关于λ表达式的内容。* ### 练习 3:用 std::函数实现回调 在本练习中,我们将利用`std::function < >`模板实现函数回调。按照以下步骤实施本练习: 1. 在 Eclipse 中打开**第 4 课**项目,然后在**项目浏览器**中,依次展开**第 4 课**和**练习 03** ,双击**练习 3.cpp** 将本练习的文件打开到编辑器中。 2. 由于这是一个基于 CMake 的项目,将当前的构建器更改为 CMake 构建(可移植)。 3. 点击**启动配置**下拉菜单,选择**新启动配置……**。将**l4 练习 3** 配置为以**练习 3** 的名称运行。 4. Click on the **Run** button. The exercise will run and produce the following output: ![Figure 4.17: Exercise 3 output (Calling empty std::function) ](img/C14583_04_17.jpg) ###### 图 4.17:练习 3 输出(调用空的 std::函数) 5. 我们要做的第一件事是防止调用空的 **std::function** 。定位功能`测试功能模板()`行`功能(42);`并替换为以下代码: ```cpp if (func) {     func(42); } else {     std::cout << "Not calling an empty func()\n"; } ``` 6. Click on the **Run** button. The exercise will run and produce the following output: ![Figure 4.18: Exercise 3 output (preventing call to empty std::function) ](img/C14583_04_18.jpg) ###### 图 4.18:练习 3 输出(防止调用空 std::函数) 7. 将`FreeFunction()`方法添加到函数`testfunction template()`: ```cpp void FreeFunction(int n) {     std::cout << "FreeFunction(" << n << ")\n"; } ``` 之前的文件中 8. 在`测试功能模板()`功能中,紧接在`if (func)`之前添加以下行: ```cpp func = FreeFunction; ``` 9. Click on the **Run** button. The exercise will run and produce the following output: ![Figure 4.19: Exercise 3 output (FreeMethod) ](img/C14583_04_19.jpg) ###### 图 4.19:练习 3 输出(FreeMethod) 10. 在`TestFunctionTemplate()`函数 ```cpp struct FuncClass {     void member(int n)     {         std::cout << "FuncClass::member(" << n << ")\n";     }     void operator()(int n)     {     std::cout << "FuncClass object(" << n << ")\n";     } }; ``` 前增加新的类定义 11. 更换线路`func = free function;`代码如下: ```cpp FuncClass funcClass; func = funcClass; ``` 12. Click on the **Run** button. The exercise will run and produce the following output: ![4.20: Exercise 3 output (Object function call override) ](img/C14583_04_20.jpg) ###### 4.20:练习 3 输出(对象函数调用覆盖) 13. 更换线路`func = func class;`代码如下: ```cpp func = std::bind(&FuncClass::member, &funcClass, std::placeholders::_1); ``` 14. Click on the **Run** button. The exercise will run and produce the following output: ![Figure 4.21: Exercise 3 output (Member function) ](img/C14583_04_21.jpg) ###### 图 4.21:练习 3 输出(成员函数) 15. 替换行`func = STD::bind(…);`代码如下: ```cpp func = [](int n) {std::cout << "lambda function(" << n << ")\n";}; ``` 16. 点击**运行**按钮。该练习将运行并产生以下输出: ![Figure 4.22: Exercise 3 output (lambda function) ](img/C14583_04_22.jpg) ###### 图 4.22:练习 3 输出(λ函数) 在本练习中,我们使用`std::function`模板实现了四种不同类型的函数回调——自由方法、类成员函数、类函数调用方法和一个 lambda 函数(我们接下来看)。 ### 什么是λ表达式? 从 C++ 11 开始,C++ 就支持`匿名函数`,也称为`lambda 表达式`,或者只是`lambdas`。lambda 表达式最常见的两种形式是: ![Figure 4.23: Most common forms of lambda expressions ](img/C14583_04_23.jpg) ###### 图 4.23:λ表达式的最常见的 n 种形式 在正常情况下,编译器能够根据 **function_body** 内的返回语句推导出 lambda 的返回类型(如上图中的形式(1)所示)。但是,如果编译器不能确定返回类型,或者如果我们希望强制一个不同的类型,那么我们可以使用 form (2)。 `【捕获】`之后的所有内容都与普通函数定义相同,只是缺少名称。Lambdas 是一种在将要使用的位置定义一个短方法(只有几行)的便捷方式。lambda 经常作为参数传递,通常不会被重用。还应该注意的是,一个 lambda 可以赋给一个变量(通常用 auto 声明)。 我们可以重新编写之前的程序,其中我们使用了`Scaler`类来使用 lambda 来实现相同的结果: ```cpp #include #include #include 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------ Lambdas with STL ------\n";     std::vector values{1,2,3,4,5};     PrintVector("Before transform", values);     std::transform(values.begin(), values.end(), values.begin(),     [] (int n) {return 5*n;}     );     PrintVector("After transform", values);     std::cout << "Complete.\n";     return 0; } ``` 当这个程序运行时,输出显示矢量被缩放了 5: ![Figure 4.24: Transform using lambda for scaling ](img/C14583_04_24.jpg) ###### 图 4.24:使用λ进行缩放的变换 本程序中的λ为`[](int n){ return 5 * n;}`并且有一个空的捕获子句`[]`。空的 capture 子句意味着 lambda 函数不会从周围的范围访问任何变量。如果没有参数传递给 lambda,那么参数子句`()`是可选的。 ### 将数据捕获到 Lambdas 中 **捕获子句**,或者**λ引入器**(来自 C++ 规范),允许匿名函数从周围的范围捕获数据供以后使用。函数与解析函数变量的作用域的这种组合被称为**闭包**。这种情况下的作用域是在 capture 子句中指定的一组变量绑定。在前一节中,我们说过 lambdas 是匿名函数。通过闭包添加变量捕获,lambdas 被更正确地识别为匿名函数对象。编译器用内联构造函数创建一个匿名类来捕获变量,并使用函数调用运算符`运算符()`。 capture 子句是由零个或多个捕获变量组成的逗号分隔列表。还有默认捕获的概念——要么通过引用,要么通过值。因此,捕获的基本语法是: * `[&]`–通过引用捕获范围内的所有自动存储持续时间变量 * `[=]`–按值捕获范围内的所有自动存储持续时间变量(制作副本) * `[ & x,y]`–通过引用捕获 x,通过值捕获 y 这由编译器转换成成员变量,这些变量由匿名`函子`类的构造函数初始化。在默认捕获(`&``=`)的情况下,它们必须排在第一位,并且只捕获正文中引用的变量。默认捕获可以通过将特定变量放入默认捕获之后的 capture 子句中来覆盖。例如,`[ &,x]`将默认通过引用捕获除`x`以外的所有内容,它将通过值捕获这些内容。 但是,虽然默认捕获很方便,但它们不是首选的捕获方法。这是因为它会导致悬空引用(通过引用捕获,当被 lambda 访问时被引用的变量不再存在)或悬空指针(通过值捕获,尤其是这个指针)。显式捕获变量更清楚,这有一个额外的好处,编译器能够警告您意外的影响(例如试图捕获全局或静态变量)。 C++ 14 在 capture 子句中引入了 **init capture** ,允许更安全的代码和一些优化。init capture 在 capture 子句中声明一个变量,并将其初始化以在 lambda 中使用。一个例子是: ```cpp int x = 5; int y = 6; auto fn = [z=x*x+y, x, y] ()             {                    std::cout << x << " * " << x << " + " << y << " = " << z << "\n";             }; fn(); ``` 这里,`z`在 capture 子句中声明并初始化,以便可以在 lambda 中使用。如果你想在 lambda 中使用 x 和 y,那么它们必须被分别捕获。正如预期的那样,当调用 lambda 时,它会产生以下输出: ```cpp 5 * 5 + 6 = 31 ``` init capture 还可以用于将可移动对象捕获到 lambda 中,或者复制类成员,如下所示: ```cpp struct LambdaCapture {   auto GetTheNameFunc ()   {     return [myName = myName] () { return myName.c_str(); };     }   std::string myName; }; ``` 这将捕获成员变量的值,并碰巧赋予它相同的名称,以便在 lambda 中使用。 默认情况下,lambda 是一个常量函数,这意味着它不能更改按值捕获变量的值。如果需要修改该值,我们需要使用第三种形式的 lambda 表达式,如下所示。 ![Figure 4.25: Another form of lambda expression ](img/C14583_04_25.jpg) ###### 图 4.25:λ表达式的另一种形式 在这种情况下,`说明符`被`可变的`代替,告诉编译器我们想要修改捕获的值。如果我们不添加可变的,并且我们试图修改一个捕获的值,那么编译器将产生一个错误。 ### 练习 4:实现 Lambdas 在本练习中,我们将实现 lambdas,以在 STL 算法的上下文中执行许多操作。按照以下步骤实施本练习: 1. 在 Eclipse 中打开**第 4 课**项目,然后在**项目浏览器**中,依次展开**第 4 课**和**练习 04** ,双击**练习 4.cpp** 将本练习的文件打开到编辑器中。 2. 由于这是一个基于 CMake 的项目,将当前的构建器更改为 CMake 构建(可移植的)。 3. 点击**启动配置**下拉菜单,选择**新启动配置……**。将**l4 练习 4** 配置为使用名称**练习 4** 运行。 4. Click on the **Run** button. The exercise will run and produce the following output: ![Figure 4.26: Initial output from Exercise 4 ](img/C14583_04_26.jpg) ###### 图 4.26:练习 4 的初始输出 5. 程序**练习 4.cpp** 包含两种方法,`PrintVector()`和`main()`。`PrintVector()`与我们在*中介绍的版本相同什么是函数对象?*。现在修改它,使用`std::for_each()`库函数和一个 lambda 代替 ranged-for 循环。将`打印矢量()`更新为: ```cpp void PrintVector(const char* prefix, std::vector& values) {     const char* sep = "";     std::cout << prefix << " = [";     std::for_each(values.begin(), values.end(),             [&sep] (int n)             {                 std::cout << sep << n;                 sep = ", ";             }     );     std::cout << "]\n"; } ``` 6. 点击**运行**按钮,我们得到和之前一样的输出。 7. Examine the lambda, we have captured the local variable `sep` by reference. Remove the `&` from `sep` and click on the **Run** button. This time the compilation fails with the following error: ![Figure 4.27: Compilation failure due to modifying read-only variable ](img/C14583_04_27.jpg) ###### 图 4.27:修改只读变量导致编译失败 8. 更改 lambda 声明以包含`可变的`说明符: ```cpp [sep] (int n) mutable {     std::cout << sep << n;     sep = ", "; } ``` 9. 点击**运行**按钮,我们得到和之前一样的输出。 10. 但是我们可以更进一步。从函数`PrintVector()`中删除`sep`的声明,并再次更改 lambda 以包括一个初始化捕获。编写以下代码来实现这一点: ```cpp [sep = ""] (int n) mutable {     std::cout << sep << n;     sep = ", "; } ``` 11. 点击**运行**按钮,我们得到和以前一样的输出。如果我们重新格式化函数`PrintVector()`它现在看起来更紧凑为: ```cpp void PrintVector(const char* prefix, std::vector& values) {     std::cout << prefix << " = [";     std::for_each(values.begin(), values.end(), [sep = ""] (int n) mutable                                   { std::cout << sep << n; sep = ", ";} );     std::cout << "]\n"; } ``` 12. 在`main()`方法中调用`PrintVector()`后添加以下行: ```cpp std::sort(values.begin(), values.end(), [](int a, int b) {return b threshold; }; auto count = std::count_if(values.begin(), values.end(), pred); std::cout << "There are " << count << " values > " << threshold << "\n"; ``` 16. Click on the **Run** button and the output now reports the number of `values > 25`: ![Figure 4.30: Output for count_if lambda stored in variable ](img/C14583_04_30.jpg) ###### 图 4.30:变量中存储的 count _ if 的输出 17. Add the following lines after the ones above and click the **Run** button: ```cpp threshold = 40; count = std::count_if(values.begin(), values.end(), pred); std::cout << "There are " << count << " values > " << threshold << "\n"; ``` 将生成以下输出: ![Figure 4.31: Erroneous output by re-using the pred lambda ](img/C14583_04_31.jpg) ###### 图 4.31:重复使用预驱动器λ的错误输出 18. 程序错误地报告有`七(7)个值>40`;应该是`三(3)`。问题是,当λ被创建并存储在变量`pred`中时,它捕获了阈值的当前值`25`。将定义`pred`的线改为如下: ```cpp auto pred = [&threshold] (int a) { return a > threshold; }; ``` 19. 点击**运行**按钮,现在输出正确报告计数: ![Figure 4.32: Correct output re-using the pred lambda ](img/C14583_04_32.jpg) ###### 图 4.32:重新使用预驱动器λ校正输出 在本练习中,我们实现了几个 lambda,使用了 lambda 表达式语法的各种特性,包括 init capture 和 mutable。 #### 使用 lambdas 虽然 lambdas 是 C++ 的一个强大特性,但是应该适当地使用它们。目标总是产生可读的代码。因此,虽然 lambda 可能很短并且很切题,但出于维护的目的,有时将功能分解到一个命名良好的方法中会更好。 ## 可变模板 在*章节 2B**不允许鸭子-模板和演绎*中,我们介绍了泛型编程和模板。在 C++ 03 之前,模板已经是 C++ 的一部分。在 C++ 11 之前,模板仅限于固定数量的参数。在某些情况下,当需要可变数量的参数时,有必要为所需参数数量的每个变体编写一个模板。或者,也有像`printf()`这样的可变函数可以接受可变数量的参数。变量函数的问题在于它们不是类型安全的,因为通过`va_arg`宏的类型转换来访问参数。C++ 11 改变了这一切,引入了变量模板,单个模板可以接受任意数量的参数。C++ 17 通过引入`constexpr` if 构造改进了变量模板的编写,该构造允许基本案例模板与“`递归`模板合并。 最好的方法是实现一个变量模板,并解释它是如何工作的。 ```cpp #include #include template T summer(T first, Args... args) {     if constexpr(sizeof...(args) > 0)           return first + summer(args...);     else         return first; } int main() {     std::cout << "\n\n------ Variadic Templates ------\n";     auto sum = summer(1, 3, 5, 7, 9, 11);     std::cout << "sum = " << sum << "\n";     std::string s1{"ab"};     std::string s2{"cd"};     std::string s3{"ef"};     std::string strsum = summer(s1, s2, s3);     std::cout << "strsum = " << strsum << "\n";     std::cout << "Complete.\n";     return 0; } ``` 当我们运行这个程序时,我们得到以下输出: ![Figure 4.33: Variadic Template Program Output ](img/C14583_04_33.jpg) ###### 图 4.33:变量模板程序输出 那么,变量模板有哪些部分?我们怎么读呢?考虑上述程序中的模板: ```cpp template T summer(T first, Args... args) {     if constexpr(sizeof...(args) > 0)         return first + summer(args...);     else         return first; } ``` 在上面的代码中: * `类型名称...Args`声明`Args`为`模板参数包`。 * `Args...args`是一个`功能参数包`,是一个参数包,其类型由`Args`给出。 * `尺寸...(参数)`返回`参数`中的包装元素数量。这是一种特殊形式的群体扩张。 * `参数...`在对`summer()`的递归调用中扩展包。 或者,您可以认为模板实际上相当于: ```cpp template T summer(T first, T1 t1, T2, ..., Tn tn) {     if constexpr(sizeof...( t1, t2, ..., tn) > 0)         return first + summer(t1, t2, ..., tn);     else         return first; } ``` 当编译器在示例程序中处理`summer(1,3,5,7,9,11)`时,它执行以下操作: * 推导出`T`是 int,`Args...`是我们的参数包,带有< int,int,int,int,int >。 * 由于包中有零个以上的参数,编译器首先生成`+ summer(参数...)`用省略号拆包模板参数转换`summer(args...)`进入`夏季(3、5、7、9、11)`。 * 然后编译器生成`summer(3,5,7,9,11)`的代码。再次,导致`第一+夏季(args...)`适用于`夏季(5、7、9、11)`。 * 重复这个过程,直到编译器必须为`summer(11)`生成代码。在这种情况下,触发`常量` if 语句的 else 子句,该语句简单地首先返回**。** **因为类型是由模板的参数决定的,所以我们不局限于具有相同类型的参数。我们已经在 STL–`STD::function`和 std::bind 中遇到了几个变量模板。 还有另一种类型的变量模板,它将其参数转发给另一个函数或模板。这种模板本身没有什么作用,但是提供了一种标准的方法。一个例子是`make_unique`模板,可以实现为: ```cpp template unique_ptr make_unique(Args&&... args) {     return unique_ptr(new T(std::forward(args)...)); } ``` `make_unique`必须调用新的运算符来分配内存,然后为类型调用合适的构造函数。调用构造函数所需的参数数量可能有很大差异。这种形式的变量模板引入了一些附加包扩展: * `Args&T3】...`表示我们有转发参考列表。 * `标准::转发<参数>(参数)...`包含扩展在一起的参数包,并且必须具有相同数量的元素–Args 是模板参数包,Args 是函数参数包。 每当我们需要将一个函数调用转发到变量模板中的另一个函数调用时,都会使用这种模式。 ### 活动 1:实现多播事件处理程序 微软最早推出`微软基础类` ( `MFC`)是在 1992 年,当时 C++ 还处于起步阶段。这意味着围绕类的许多设计选择都受到了限制。例如,事件的处理程序通常通过`OnEventXXX()`方法进行路由。这些通常使用宏作为从 MFC 类派生的类的一部分来配置。您的团队负责实现多播事件处理程序,更像 C#中可用的委托,使用包含函数对象的模板,并导致变量模板来实现变量参数列表。 在 C#中,您按如下方式声明委托: ```cpp delegate int Handler(int parameter); ``` 这使得 Handler 成为一种可以被赋值的类型,可以被调用。这基本上就是`std::function < >`在 C++ 中给我们提供的东西,除了多点投射的能力。您的团队被要求开发一个模板类`委托`,它可以以与 C#委托相同的方式执行。 * 该委托将获取一个`变量参数列表`,但只返回`void` * `运算符+=`将用于向委托添加新的回调 * 它将使用任一语法`委托来调用。通知(……)`或`代表(……)` 按照以下步骤开发代理模板: 1. 从**第 4 课/练习 01** 文件夹加载准备好的项目,并将项目的当前构建器配置为可移植构建。 2. 构建项目,配置启动器并运行单元测试(未通过一个虚拟测试)。建议测试运行者的名称为 **L4delegateTests** 。 3. 实现一个`委托`类,该类可以用所有需要的方法包装单个处理程序,并支持回调的单个 int 参数。 4. 更新模板类以支持多重转换。 5. 将`委托`类转换为一个模板,该模板可以接受一个模板参数,该参数定义回调使用的参数类型。 6. 将`委托`模板转换为变量模板,该模板可以接受零个或多个参数来定义传递给回调的类型。 完成上述步骤后,预期输出如下所示: ![Figure 4.34: Output from the successful implementation of Delegate ](img/C14583_04_34.jpg) ###### 图 4.34:成功实现委托的输出 #### 注意 这项活动的解决方案可以在第 673 页找到。 ## 总结 在这一章中,我们实现了一种数据和方法隐藏设计方法,PIMPL,它具有减少依赖和减少构建时间的额外好处。然后,我们将函数对象直接实现为自定义类,然后实现为 lambda 函数。然后,我们通过深入到可变模板中来扩展我们的模板编程技能,最终得到一个可用于事件回调处理的模板。在下一章中,我们将学习如何使用 C++ 的特性来开发多线程程序,并通过并发结构来管理它们的协作。** ================================================ FILE: docs/adv-cpp/06.md ================================================ # 六、哲学家的晚餐——线程和并发 ## 学习目标 本章结束时,您将能够: * 创建同步和异步多线程应用 * 应用同步来处理数据危险和竞争条件 * 用 C++ 线程库原语开发高效的多线程代码 * 使用多线程闭包的移动语义创建线程 * 用未来、承诺和异步实现线程通信 在本章中,我们将阐明多线程编程中基本术语之间的区别,学习如何编写多线程代码,找出 C++ 标准库为数据访问同步提供了哪些资源,学习如何防止我们的代码遇到竞争条件和死锁。 ## 简介 在前一章中,我们讨论了 C++ 中不同类型的依赖和耦合。我们研究了如何在 C++ 中实现常见的 API 设计模式和习惯用法,标准库提供了哪些数据结构,以及它们的功效。我们还学习了如何使用功能对象、lambdas 和捕获。这些知识将帮助我们学习如何编写清晰高效的多线程程序。 本章的标题包含并发编程中最重要的同步问题的名称——哲学家的晚餐。三言两语,这个定义如下。 三位哲学家正坐在一张圆形餐桌旁,面前摆着几碗寿司。筷子放在每个相邻的哲学家之间。一次只有一个哲学家能用两根筷子吃他们的寿司。也许每个哲学家都会拿一根筷子,然后等到有人放弃另一根筷子。哲学家是三个工作过程的类比,是共享资源的筷子。“谁先拿两根筷子”象征着**比赛状态**。当每个哲学家拿着一根筷子,等待另一根筷子出现时,就会导致**僵局**。这个类比解释了多线程过程中会出现什么问题。 我们将以对主要多线程概念的简单介绍开始本章。我们将考虑同步、异步和线程执行之间的区别。使用简单明了的例子,我们将从同步、数据危险和比赛条件开始。我们将找出它们为什么会出现在我们的代码中,以及我们如何管理它们。本章的下一部分专门介绍用于线程执行的 C++ 标准库。通过示例,我们将了解如何以及何时使用线程库原语,以及**如何移动语义**与线程进行交互。我们还将使用**期货**、**承诺**和**异步**从线程接收结果。 这一章将以一个具有挑战性的活动结束,在这个活动中,我们将通过模拟参观者和画廊工作人员来创建一个艺术画廊模拟器。我们将开发一个多线程生成器,它将同时创建和删除美术馆的访客。接下来,我们将创建一个多线程类,负责在画廊中移动访问者。它们将使用同步技术相互交互。最后,我们将创建线程安全存储,其实例将从不同的线程访问。 在下一节中,我们将阐明并发编程概念之间的细微区别:**同步**、**异步**和**线程**执行。 ## 同步、异步和线程执行 并发编程的概念之间有细微的区别:`同步`、`异步`和`线程执行`。为了澄清这一点,我们将从最开始,从并发和并行程序的概念开始。 ### 并发 `并发`的思想是多个任务同时执行。`并发`没有说明如何实现同时性。它仅表示在给定的时间段内将完成多个任务。任务可以是`从属`、`并行`、`同步`或`异步`。下图显示了并行工作的概念: ![Figure 5.1: The abstraction of the concurrency - a few people working on the same computer ](img/C14583_05_01.jpg) ###### 图 5.1:并发的抽象——几个人在同一台计算机上工作 在上图中,三个人同时在一台计算机上工作。我们对他们这样做的方式不感兴趣,这与抽象的这个层次无关。 ### 平行度 **并行**发生在多个任务同时执行的时候。由于硬件能力,这些任务并行工作。并行性的最好例子是多核处理器。对于并行执行,任务被分成完全独立的子任务,在不同的处理器内核中执行。之后,执行的结果可以合并。请看下图,了解并行性的概念: ![](img/C14583_05_02.jpg) ###### 图 5.2:并行性的抽象——所有的任务都由不同的人执行;他们不会互相影响 在上图中,有三个人同时在自己的电脑上工作——嗯,他们是并行工作的。 #### 注意 `并发`和`并行`不是一回事。`并行`补充并发。它告诉我们任务是如何执行的:它们彼此独立,并在不同的计算单元中运行,即处理器或内核。 现在,我们将顺利地转向线程执行概念。当我们谈论线程时,我们指的是执行线程。这是操作系统的抽象,允许我们同时执行几个任务。请记住,整个程序在单独的进程中执行。操作系统为进程分配**地址空间**、**处理器寄存器**,以及一些额外的资源。所有的工作线程都是在进程中创建的,并且共享相同的资源。每个进程至少有一个执行`main()`函数的线程。我们可以创建一个新的线程来执行,并分配一个开始函数作为这个线程的起点。 #### 注意 处理器的地址空间和寄存器称为**线程上下文**。当操作系统中断线程的工作时,它必须存储当前线程的上下文,并加载下一个线程的上下文。 让我们考虑在下面的例子中创建一个新的线程。要创建一个新的线程,我们必须包含一个`<线程>`头文件。它包含用于管理线程的类和函数。实际上,有几种可能的方法来创建`std::thread`对象和执行线程,如下所示: * 创建一个`标准::线程`对象,无需显式初始化。记住,线程需要一个启动函数来运行它的作业。我们没有指出哪个函数是这个线程的主要函数。这意味着没有创建执行线程。让我们看看下面的代码示例,其中我们创建了一个空的`std::thread`对象: ```cpp #include int main() {   std::thread myThread;     return 0; } ``` * 创建一个`std::thread`对象,并传递一个指向函数的指针作为构造函数参数。现在,执行线程将被创建,并将从我们在构造函数中传递的函数开始它的工作。让我们看看下面的代码示例: ```cpp #include #include void printHello() {     std::cout << "hello" << std::endl; } int main() {   std::thread myThread(printHello);   myThread.join();   return 0; } ``` 这里,我们创建了一个`std::thread`对象,并用函数指针对其进行了初始化。这是一个简单的函数,返回`void`,不取任何参数。然后,我们告诉主线程使用`join()`函数等待新线程完成。我们总是要`连接()`或`分离()`一个线程,直到`std::thread`对象的范围结束。如果我们不这样做,我们的应用将被操作系统使用`标准::终止`()函数终止,该函数在`标准::线程`析构函数中调用。除了函数指针之外,我们还可以传递任何可调用的对象,比如`lambda`、`std::function`,或者带有重载`运算符()`的类。 #### 注意 执行线程可以在 **std::thread** 对象销毁之前完成工作。它也可以在执行线程完成工作之前被析构。在销毁对象之前,始终将对象**标准::螺纹**连接到()或**分离()**。 现在我们已经知道了创建线程的主要语法,我们可以继续下一个重要的概念。让我们找出同步、异步和多线程执行的含义。 ### 同步执行 术语“同步执行”意味着每个子任务都将按顺序逐一执行。换句话说,这意味着如果我们有几个任务要执行,每个任务只能在前一个任务完成后才可以开始工作。这个术语没有指定执行任务的方式,也没有指定任务是在一个线程中执行还是在几个线程中执行。它只告诉我们执行的顺序。让我们回到哲学家晚餐的例子。在单线程世界里,哲学家们会一个接一个地吃东西。 第一个哲学家拿了两根筷子,吃他们的寿司。然后,第二个哲学家拿了两根筷子,吃了他们的寿司。他们轮流,直到所有人都吃完寿司。请看下图,它代表了一个线程中四个任务的同步执行: ![Figure 5.3: Synchronous execution in a single thread ](img/C14583_05_03.jpg) ###### 图 5.3:单线程中的同步执行 这里,每个任务都等待前一个任务完成。任务也可以在多个线程中同步执行。考虑下图,它表示在多个线程中同步执行四个任务。同样,每个任务都等待前一个任务完成: ![](img/C14583_05_04.jpg) ###### 图 5.4:多线程中的同步执行 在这种情况下,每个任务都在一个单独的线程中启动,但只是在前一个线程完成其工作之后。在一个多线程的世界里,哲学家们仍然会一个接一个地吃东西,但差别很小。现在,他们每个人都有自己的筷子,但只能按照严格的顺序吃饭。 #### 注意 `同步执行`表示每个任务的完成时间同步。任务的执行顺序是这里的重点。 让我们考虑在下面的代码示例上同步执行。当我们在一个线程中运行任务时,我们只是调用通常的函数。例如,我们实现了四个向终端打印消息的功能。我们以同步、单线程的方式运行它们: ```cpp #include void printHello1() {     std::cout << "Hello from printHello1()" << std::endl;     } void printHello2() {     std::cout << "Hello from printHello2()" << std::endl;     } void printHello3() {     std::cout << "Hello from printHello3()" << std::endl;     } void printHello4() {     std::cout << "Hello from printHello4()" << std::endl;     } int main() {     printHello1();     printHello2();     printHello3();     printHello4();     return 0; } ``` 这里,我们逐个调用所有函数,每个下一个函数都在前一个函数执行之后运行。现在,让我们在不同的线程中运行它们: ```cpp #include #include void printHello1() {     std::cout << "Hello from printHello1()" << std::endl;     } void printHello2() {     std::cout << "Hello from printHello2()" << std::endl;     } void printHello3() {     std::cout << "Hello from printHello3()" << std::endl;     } void printHello4() {     std::cout << "Hello from printHello4()" << std::endl;     } int main() {     std::thread thread1(printHello1);     thread1.join();     std::thread thread2(printHello2);     thread2.join();     std::thread thread3(printHello3);     thread3.join();     std::thread thread4(printHello4);     thread4.join();     return 0; } ``` 在前面的代码示例中,我们创建了四个线程,并立即将它们连接起来。因此,每个线程在运行之前都完成了它的工作。如您所见,任务没有任何变化——它们仍然以严格的顺序执行。 ### 异步执行 在这种情况下,可以同时执行几个任务,而不会阻塞任何线程的执行。通常,主线程启动异步操作并继续执行。执行完成后,结果被发送到主线程。通常,执行异步操作与为其创建单独的线程无关。该任务可以由其他人执行,例如另一个计算设备、远程网络服务器或外部设备。让我们回到哲学家晚餐的例子。 在`异步执行`的情况下,所有的哲学家都会有自己的筷子,并且会相互独立吃饭。寿司做好了,服务员上桌后,他们都开始吃饭,可以在自己的时间内吃完。 #### 注意 在`异步执行`中,由于所有的任务都是相互独立工作的,所以知道每个任务的完成时间并不重要。 请看下图,它代表了多线程中四个任务的异步执行: ![Figure 5.5: Asynchronous execution in multiple threads ](img/C14583_05_05.jpg) ###### 图 5.5:多线程中的异步执行 他们每个人都是在不同的时间开始和结束的。让我们用一个代码示例来考虑这个异步执行。例如,我们实现了四个向终端打印消息的功能。我们用不同的线程运行它们: ```cpp #include #include #include void printHello1() {     std::cout << "Hello from thread: " << std::this_thread::get_id() << std::endl;     } void printHello2() {     std::cout << "Hello from thread: " << std::this_thread::get_id() << std::endl;     } void printHello3() {     std::cout << "Hello from thread: " << std::this_thread::get_id() << std::endl;     } void printHello4() {     std::cout << "Hello from thread: " << std::this_thread::get_id() << std::endl;     } int main() {     std::thread thread1(printHello1);     std::thread thread2(printHello2);     std::thread thread3(printHello3);     std::thread thread4(printHello4);     thread1.detach();     thread2.detach();     thread3.detach();     thread4.detach();     using namespace std::chrono_literals;     std::this_thread::sleep_for(2s);     return 0; } ``` 让我们看看这里会发生什么。我们使用了前面例子中的四个函数,但是它们有一点改变。我们使用`STD::this _ thread::get _ ID()`函数添加了线程唯一 ID 的打印。该函数返回`std::thread::id`对象,该对象代表线程的唯一 id。这个类为输出和比较重载了操作符,所以我们可以用不同的方式使用它。例如,我们可以检查线程标识,如果它是主线程的标识,我们可以执行一个特殊的作业。在我们的例子中,我们可以将线程标识打印到终端。接下来,我们创建了四个线程并分离它们。这意味着没有线程会等待另一个线程完成工作。从这一刻起,它们变成了**守护线程**。 他们将继续他们的工作,但是没有人知道这件事。然后,我们使用`STD::this _ thread::sleep _ for(2s)`函数让主线程等待两秒钟。我们这样做是因为当主线程完成它的工作时,应用将停止,我们将无法在终端中查看分离线程的输出。以下屏幕截图是终端输出的示例: ![Figure 5.6: The result of an example execution ](img/C14583_05_06.jpg) ###### 图 5.6:示例执行的结果 在您的 IDE 中,输出可能会随着执行顺序的未定义而改变。异步执行的一个真实例子可以是一个互联网浏览器,您可以在其中打开多个选项卡。当一个新的选项卡打开时,应用启动一个新的线程并分离它们。尽管线程独立工作,但它们可以共享一些资源,如文件处理程序,来写日志或做其他事情。 #### 注意 `std::thread`有一个名为`get_id()`的成员函数,返回`std::thread`实例的唯一 id。如果`std::thread`实例未初始化或已连接或分离,`get_id()`将返回默认的`std::thread::id`对象。这意味着没有执行线程与当前的`std::thread`实例相关联。 让我们用一些伪代码来展示一个由另一个计算单元完成计算的例子。例如,假设我们开发了一个通过货币兑换进行计算的应用。用户输入一种货币的金额,选择另一种货币进行兑换,应用向他们显示该货币的金额。后台应用向保存所有货币汇率的远程服务器发送请求。 远程服务器计算给定货币的金额,并将结果发回。此时,您的应用会显示一个进度条,并允许用户执行其他操作。当它收到结果时,它会在窗口上显示它们。让我们看看下面的代码: ```cpp #include void runMessageLoop() {     while (true)     {         if (message)         {             std::thread procRes(processResults, message);             procRes.detach();         }     } } void processResults(Result res) {     display(); } void sendRequest(Currency from, Currency to, double amount) {     send(); } void displayProgress() { } void getUserInput() {     Currency from;     Currency to;     double amount;     std::thread progress(displayProgress);     progress.detach();     std::thread request(sendRequest, from, to, amount);     request.detach(); } int main() {     std::thread messageLoop(runMessageLoop);     messageLoop.detach();     std::thread userInput(getUserInput);     userInput.detach();         return 0; } ``` 让我们看看这里会发生什么。在`main()`函数中,我们创建了一个名为`messageLoop`的线程,该线程执行`runMessageLoop()`函数。一些检查服务器是否有新结果的代码可以放在这个函数中。如果收到新的结果,它会创建一个新的线程`procures`,该线程将在一个窗口中显示结果。我们还在`main()`函数中创建了另一个线程`userInput`,它从用户那里获取货币和金额,并创建了一个新的线程`request`,它将向远程服务器发送请求。发送请求后,它会创建一个新的线程`进度`,该线程将显示一个进度条,直到收到结果。因为所有的线程都是分离的,所以它们能够独立工作。当然,这只是伪代码,但主要思想很清楚——我们的应用向远程服务器发送请求,远程服务器为我们的应用执行计算。 让我们用日常生活中的一个例子来复习一下我们所学的并发概念。这是一个背景,你必须编写一个应用,并提供与之相关的所有文档和架构概念: * 单线程工作:你自己写。 * 多线程工作:你邀请你的朋友一起写一个项目。有人编写架构概念,有人负责文档工作,你专注于编码部分。所有参与者相互交流以澄清任何问题并共享文档,例如关于规格的问题。 * 平行工作:任务是分的。有人为项目编写文档,有人设计图表,有人编写测试用例,你独立工作。参与者根本不交流。 * 同步工作:在这种情况下,你们每个人都无法理解他们应该做什么。因此,你们都决定一个接一个地工作。当架构工作完成时,开发人员开始编写代码。然后,当开发工作完成时,有人开始编写文档。 * 异步工作:在这种情况下,你雇佣一家外包公司来完成项目。当他们在开发项目时,你将会从事一些其他的任务。 现在,让我们将我们的知识应用于实践,并解决一个练习,看看它是如何工作的。 ### 练习 1:以不同的方式创建线程 在本练习中,我们将编写一个创建四个线程的简单应用;其中两个将以同步方式工作,两个以异步方式工作。它们都会打印一些符号到终端,这样我们就可以看到操作系统是如何切换线程执行的。 #### 注意 在项目设置中添加 pthread 链接器标志,让编译器知道您将使用线程库。对于 Eclipse IDE,您可以按照以下路径执行此操作:**项目** - > **属性**->**C/c++ Build**->**设置** - > **G++ 链接器** - > **杂项** - > **链接器标志输入“-pthread”**。该路径对`Eclipse 版本有效:3.8.1`,不同版本可能有所不同。 执行以下步骤完成本练习: 1. 包括一些支持线程化的头文件,即`<【线程化】>`,支持流化,即`<【iostream】>`,支持功能对象,即`<【功能化】>` : ```cpp #include #include #include ``` 2. 实现一个自由函数`打印数字()`,在`中为`循环打印从 0 到 100 的数字: ```cpp void printNumbers() {     for(int i = 0; i < 100; ++ i)     {         std::cout << i << " ";     }     std::cout << std::endl; } ``` 3. 实现一个可调用对象,即一个带有重载**运算符()**的 **Printer** 类,该类在**循环中打印一个从 0 到 100000 的“*”符号。对于每一次 **200** 迭代,打印一个新的线符号以获得更易读的输出: ```cpp class Printer {     public:     void operator()()     {         for(int i = 0; i < 100000; ++ i)         {             if (!(i % 200))             {                 std::cout << std::endl;             }             std::cout << "*";         }     } }; ```** 4. 进入`main()`功能,然后创建一个名为`print reverse`的 lambda 对象,该对象在`中为`循环打印从 100 到 0 的数字: ```cpp int main() {     auto printRevers = []()     {         for(int i = 100; i >= 0; --i)         {             std::cout << i << " ";         }         std::cout << std::endl;     };     return 0; } ``` 5. 实现一个名为 **printOther** 的 **std::function** 对象,该对象在循环的**中打印从 **0** 到 **100000** 的“^”符号。每重复 **200 次**,打印一个新的线符号,以便输出更易读: ```cpp std::function printOther = []() {     for(int i = 0; i < 100000; ++ i)     {         if (!(i % 200))         {             std::cout << std::endl;         }         std::cout << "^";     } }; ```** 6. 创建第一个线程`thr1`,并将`printNumbers`自由函数传递给它的构造函数。加入其中: ```cpp std::thread thr1(printNumbers); thr1.join(); ``` 7. 创建第二个线程`thr2`,并将`print reverse`lambda 对象传递给它的构造函数。加入其中: ```cpp std::thread thr2(printRevers); thr2.join(); ``` 8. 创建名为`打印`的`打印机`类的实例。创建第三个线程`thr3`,并用`打印`对象初始化。使用`分离()`方法将其分离: ```cpp Printer print; std::thread thr3(print); thr3.detach(); ``` 9. 创建最后一个线程`thr4`,并用`printOther`对象初始化它。拆下来: ```cpp std::thread thr4(printOther); thr4.detach(); ``` 10. 在退出`main()`函数之前,添加`std::getchar()`函数调用。这避免了关闭应用。我们将有可能看到分离线程是如何工作的: ```cpp std::getchar(); ``` 11. Run this code in your editor. You will see that `thr1` starts execution and the program waits. After `thr1` has finished, `thr2` starts execution and the program waits. This is an example of synchronous execution. After `thr2` has finished its work, threads `thr3` and `thr4` start execution. They are detached, so the program can proceed with the execution. In the following output, you will see that the symbols are mixed. This happens because the operating system performs interruptions and the threads work at the same time. 您的输出将类似于以下内容: ![](img/C14583_05_07.jpg) ###### 图 5.7:练习执行的结果 在本练习中,我们实现了四种不同的初始化线程的方法:使用自由函数、lambda 对象、可调用对象和`std::function`对象。还有一些初始化线程的方法,但是我们将在下一节中考虑它们。我们还回顾了如何在多线程中实现同步程序。我们还试图实现异步程序,并看到线程确实同时独立工作。在下一节中,我们将了解数据危险和竞争条件,以及如何通过使用同步技术来避免它们。 ## 查看同步、数据危险和比赛条件 多线程编程的关键挑战是知道线程如何处理**共享数据**。共享数据,也称为资源,不仅是变量,也是文件描述符和环境变量,甚至是 Windows 注册表。例如,如果线程只是读取数据,那么就没有问题,也不需要同步。但是,如果至少有一个线程编辑了数据,**比赛条件**可能会出现。通常,对数据的操作不是原子的,也就是说,它们需要几个步骤。即使是最简单的数值变量的增量操作也是在以下三个步骤中执行的: 1. 读取变量值。 2. 增加它。 3. 写下新值。 由于操作系统中断,线程可以在完成操作之前停止。例如,我们有线程 A 和 B,并且有一个等于 0 的变量。 线程 A 开始增量: 1. 读取变量值(var = 0)。 2. 递增(tmp = 1)。 3. 被操作系统中断。 线程 B 开始增量: 1. 读取变量值(var = 0)。 2. 递增(tmp = 1)。 3. 写入新值(var = 1)。 4. 被操作系统中断。 线程 A 继续递增: 1. 写入新值(var = 1)。 因此,我们期望变量在工作完成后等于 2,但事实上,它等于 1。请看下图,以便更好地理解这个例子: ![Figure 5.8: Two threads increment the same shared variable ](img/C14583_05_08.jpg) ###### 图 5.8:两个线程递增同一个共享变量 让我们回到哲学家的晚餐类比。最初的问题是一个哲学家只有一根筷子。如果他们都饿了,那么他们会赶紧去拿两根筷子。第一个抓起两根筷子的哲学家会第一个吃饭,其他人必须等待。他们将争夺木棒。 现在,让我们将我们的知识应用到实践中,并编写一些代码,看看竞争条件如何出现在我们的代码中,并可能破坏我们的数据。 ### 练习 2:写一个比赛条件的例子 在本练习中,我们将编写一个简单的应用来演示比赛条件。我们将创建一个“先检查后行动”比赛条件的经典示例。我们将创建一个线程,执行两个数的除法。我们将通过引用传递这些数字。经过检查,如果股息等于 0,我们将设置一个小超时。此时在主线程中,我们将把被除数设置为 0。当子线程醒来时,它将执行到 0 的除法。这将导致应用崩溃。我们还将添加一些日志来查看执行流程。 #### 注意 默认情况下,所有变量在传递给线程时都会被复制。要将变量作为引用传递,请使用`std::ref()`函数。 首先,我们实现没有竞争条件的代码,并确保它按预期工作。请执行以下步骤: 1. 包括支持线程的头文件,即`<线程>`,支持流的头文件,即`< iostream >`,以及支持功能对象的头文件,即`<【功能性】>` : ```cpp #include #include #include ``` 2. 实现`除()`函数,该函数执行两个整数的除。通过引用传递`除数`和`被除数`变量。检查股息是否等于 0。然后,添加日志: ```cpp void divide(int& divisor, int& dividend) {     if (0 != dividend)     {         std::cout << "Dividend = " << dividend << std::endl;         std::cout << "Result: " << (divisor / dividend) << std::endl;         }     else     {         std::cout << "Error: dividend = 0" << std::endl;     } } ``` 3. 进入`main()`函数,创建两个名为`除数`和`被除数`的整数,并用任意非零值初始化: ```cpp int main() {     int divisor = 15;     int dividend = 5;     return 0; } ``` 4. Create the `thr1` thread, pass the `divide` function, use `divisor` and `dividend` by reference, and then detach the thread: ```cpp std::thread thr1(divide, std::ref(divisor), std::ref(dividend)); thr1.detach(); std::getchar(); ``` #### 注意 在`std::this_thread`命名空间中,有一个名为`sleep_for`的函数会在给定的时间段内阻塞线程。作为一个参数,它需要`标准::时间::持续时间`-一个模板类来表示时间间隔。 5. Run this code in your editor. You will see that the `divide()` function works correctly in `thr1`. The output looks as follows: ![Figure 5.9: The result of the correct exercise execution ](img/C14583_05_09.jpg) ###### 图 5.9:正确执行练习的结果 现在,我们将继续并做出改变,以展示比赛条件。 6. 如果处于状态,则返回到该功能并在 **2s** 中为**之后的子线程设置睡眠时间。添加日志: ```cpp if (0 != dividend) {     std::cout << "Child thread goes sleep" << std::endl;     using namespace std::chrono_literals;     std::this_thread::sleep_for(2s);     std::cout << "Child thread woke up" << std::endl;     std::cout << "Dividend = " << dividend << std::endl;     std::cout << (divisor / dividend) << std::endl; } ```** 7. Go back to the `main()` function and set the sleeping time in `1s` for the main thread. After that, set the `dividend` variable to `0`. Add the logs: ```cpp std::cout << "Main thread goes sleep" << std::endl; using namespace std::chrono_literals; std::this_thread::sleep_for(1s); std::cout << "Main thread woke up" << std::endl; dividend = 0;    std::cout << "Main thread set dividend to 0" << std::endl; ``` #### 注意 `STD::chrono _ 常值`命名空间包含用于时间表示的常值:``h``用于`小时`,``分钟``用于`分钟`,``s``用于`秒`,``ms``用于`毫秒`,``us``用于`微秒`和` `[要使用它们,您只需将它们添加到数字的末尾,例如 1、1 分钟、1 小时等。 8. 在退出`main()`函数之前,添加`std::getchar()`函数调用。这避免了我们关闭应用,我们将有可能看到分离的线程是如何工作的: ```cpp std::getchar(); ``` 9. 在编辑器中运行这段代码。你会看到主线程休眠`1s`。然后,子线程进入`if`状态,休眠`2s`,这意味着它验证了一个`红利`,它不等于`0`。然后,主线程唤醒并将`被除数`变量设置为 0。然后,子线程醒来并执行除法。但是因为现在`红利`等于`0`,应用崩溃了。如果在调试模式下运行此示例,您将看到一个`SIGFPE 异常`,并显示一条消息:“算术异常”。您将获得以下输出: ![Figure 5.10: The result of the exercise’s execution with race conditions ](img/C14583_05_10.jpg) ###### 图 5.10:比赛条件下练习的执行结果 在本练习中,我们考虑了“先检查后行动”的比赛条件。我们已经为线程设置了睡眠周期来模拟操作系统中断,但是在现实世界的程序中,这种情况很可能会发生,但也可能不会。这完全取决于操作系统及其调度程序。这使得调试和修复比赛状态变得非常困难。为了避免本例中的竞争情况,我们可以采取以下几种方式: * 将变量的副本传递给线程函数,而不是传递引用。 * 使用标准库原语在线程之间同步对共享变量的访问。 * 在主线程将一个`被除数`值变为 0 之前加入子线程。 让我们再看几个方法来解决这个比赛状态。所有这些都依赖于你试图实现的任务。在下一节中,我们将考虑由 C++ 标准库提供的同步原语。 ### 数据危害 以前,我们认为最无害的例子,但有时,存在数据被损坏的情况,这导致未定义的程序行为或异常终止。这种由于比赛条件或简单的错误设计造成的数据损坏被称为**数据危险**。一般来说,这个术语意味着一项工作的最终结果取决于线程的执行顺序。如果不同的线程使用共享数据或全局变量,那么由于不同线程执行任务的顺序不正确,结果可能会有所不同。这是由于多线程数据之间的依赖性造成的。这种依赖性问题有条件地分为三类: * 一**真相依** : **写后读** (RAW) * 一个**反依赖** : **读完**写(WAR) * 一个**输出依赖** : **写后写** (WAW) ### 原始相关性 当一个线程计算另一个线程使用的值时,就会发生 RAW 依赖关系。例如`线程 A`应该完成它的工作,并将结果写入一个变量。`线程 B`必须读取这个变量的值并完成它的工作。在伪代码中,如下所示: ```cpp Thread A: a = doSomeStuff(); Thread B: b = a - doOtherStuff(); ``` 如果`线程 B`先执行,就会出现困难。这会导致`线程 B`读取无效值。应该严格保证线程的执行顺序。`线程 B`必须读取变量的值,但只有在`线程 A`写完之后。否则,将导致未定义的行为。下图将帮助您阐明导致数据危险的原始数据依赖性: ![Figure 5.11: RAW data dependency between two threads ](img/C14583_05_11.jpg) ###### 图 5.11:两个线程之间的原始数据依赖性 ### 战争属地 当一个线程更改另一个线程使用的数据时,就会出现 **WAR 依赖关系**。例如,`线程 A`必须读取一个变量的值并完成它的工作。之后,`线程 B`应该做好自己的工作,将结果写入一个变量。在伪代码中,如下所示: ```cpp Thread A: b = a - doSomeStuff(); Thread B: a = doOtherStuff(); ``` 如果`线程 B`先执行,就会出现困难。会导致`线程 B`在`线程 A`读取之前改变值。应该严格保证线程的执行顺序。`线程 B`只有在`线程 A`读取变量的值后,才应该将新值写入变量。下图将帮助您阐明导致数据危险的原始数据依赖性: ![Figure 5.12: WAR data dependency between two threads ](img/C14583_05_12.jpg) ###### 图 5.12:两个线程之间的 WAR 数据依赖关系 ### WAW 相关性 当几个线程改变同一个变量的值,并且一些线程为其计算进行读取时,就会出现 **WAW 依赖关系**。例如,`线程 A`执行其作业并将结果写入变量。`线程 B`读取变量值并执行其作业。`线程 C`执行其作业,并将结果写入同一个变量。在伪代码中,如下所示: ```cpp Thread A: a = doSomeStuff(); Thread B: b = a - doOtherStuff(); Thread C: a = doNewStuff(); ``` 如果`线程 C`在线程 A 和 B 之前执行,将会出现困难。这将导致`线程 B`读取预计不会被读取的值。应该严格保证线程的执行顺序。`线程 C`必须向变量写入一个新值,但前提是`线程 A`已经写入了它的值,`线程 B`已经读取了它。下图将帮助您阐明导致数据危险的 WAW 数据依赖性: ![Figure 5.13: WAW data dependency between two threads ](img/C14583_05_13.jpg) ###### 图 5.13:两个线程之间的 WAW 数据依赖关系 ### 资源同步 为了防止竞争和数据危害,有一个共享数据锁定机制,其中一个流打算更改或读取这些数据。这个机制叫做**资源同步**。对于同步,我们需要分配改变或读取共享资源的代码片段。这样的代码片段被称为`关键部分`。同步包括当一个线程进入时阻塞关键部分。也打算执行这个关键部分的代码的其他线程将被阻塞。当执行关键部分的线程离开它时,锁被释放。然后,故事会随着下一个线索重复。 考虑前面的例子,有一个增量,但是现在有同步访问。请记住,我们有线程 A 和 B,并且有一个等于 0 的变量。 线程 A 开始增量: 1. 进入临界区并锁定。 2. 读取变量值(var = 0)。 3. 递增(tmp = 1)。 4. 被操作系统中断。 线程 B 开始增量: 1. 试图进入临界区;它被锁定了,所以线程正在等待。 线程 A 继续递增: 1. 写入新值(var = 1)。 线程 B 继续递增: 1. 进入临界区并锁定。 2. 读取变量值(var = 1)。 3. 递增(tmp = 2)。 4. 写入新值(var = 2)。 两个线程完成后,变量包含正确的结果。因此,同步确保共享数据不会被损坏。请看下图,以更好地理解这个例子: ![Figure 5.14: Two threads increment the same shared variable in a synchronized way ](img/C14583_05_14.jpg) ###### 图 5.14:两个线程以同步的方式增加同一个共享变量 突出关键部分并预测非同步访问的可能后果是一项非常困难的任务。因为过度同步否定了多线程工作的本质。然而,如果两个或三个线程在一个关键部分上工作得相当快,那么程序中可能会有几十个线程在关键部分被阻塞。这将大大降低程序的速度。 ### 事件同步 还有另一种同步线程工作的机制–**事件同步**。这意味着当其中一个线程暂停其工作,直到另一个线程发出某个事件发生的信号时,线程的工作才会同步。例如,有`线程 A`,它接收来自另一个进程的消息。它将消息写入队列并等待新消息。还有一个线程`线程 B`,处理这些消息。它从队列中读取消息,并对它们执行一些操作。当没有消息时,`线程 B`正在休眠。当`线程 A`收到新消息时,唤醒`线程 B`并进行处理。下图清楚地说明了两个线程的事件同步: ![Figure 5.15: Event synchronization of two threads ](img/C14583_05_15.jpg) ###### 图 5.15:两个线程的事件同步 然而,即使在同步代码中也会出现竞争条件的另一个原因——类的有缺陷的接口。为了理解这是什么,让我们考虑以下示例: ```cpp class Messages {     public:     Messages(const int& size)     : ArraySize(size)     , currentIdx(0)     , msgArray(new std::string[ArraySize])     {}     void push(const std::string& msg)     {         msgArray[currentIdx++ ] = msg;     }     std::string pop()     {         auto msg = msgArray[currentIdx - 1];         msgArray[currentIdx - 1] = "";         --currentIdx;         return msg;     }     bool full()     {         return ArraySize == currentIdx;     }     bool empty()     {         return 0 == currentIdx;     }     private:     const int ArraySize;     int currentIdx;     std::string * msgArray; }; ``` 这里,我们有一个名为`Messages`的类,它有一个动态分配的字符串数组。在构造函数中,它获取数组的大小并创建一个给定大小的数组。它有一个函数`full()`,如果数组已满则返回`true`,否则返回`false`。它还有一个`empty()`函数,如果数组为空则返回 true,否则返回 false。在推送新值和检查数组是否为空之前,以及在从数组弹出新值之前,用户有责任检查数组是否已满。这是类接口不良导致竞争条件的一个例子。即使我们用锁保护`push()`和`pop()`功能,竞态条件也不会消失。让我们看看下面使用`消息`类的例子: ```cpp int main() {     Messages msgs(10);     std::thread thr1([&msgs](){     while(true)     {         if (!msgs.full())         {             msgs.push("Hello");         }         else         {             break;         }     }});     std::thread thr2([&msgs](){     while(true)     {         if (!msgs.empty())         {             std::cout << msgs.pop() << std::endl;         }         else         {             break;         }     }});     thr1.detach();     thr2.detach();     using namespace std::chrono_literals;     std::this_thread::sleep_for(2s);     return 0; } ``` 这里,我们创建了一个`msgs`变量,然后创建了第一个线程,该线程将值推送到`msgs`。然后,我们创建了第二个线程,它从数组中弹出值并分离它们。即使我们通过使用锁定机制来保护所有功能,其中一个线程也可以检查数组的大小,并且可以被操作系统中断。此时,另一个线程可以更改数组。当第一个线程继续工作时,它可以尝试推进到完整数组或从空数组弹出。因此,同步只有在设计良好的配对中才有效。 ### 死锁 还有一个同步问题。让我们回到哲学家晚餐的例子。最初的问题是一个哲学家只有一根筷子。所以,他们可以通过互相分享筷子来一个接一个地吃寿司。虽然他们需要很长时间才能吃完寿司,但他们都会吃得很好。但是如果他们每个人同时拿着一根筷子,不想分享第二根筷子,他们就不能吃寿司,因为他们每个人都将永远等待第二根筷子。这会导致**死锁**。当两个线程正在等待另一个线程继续其工作时,就会发生这种情况。死锁的原因之一是当一个线程加入另一个线程,但是另一个线程加入第一个线程。因此,当两个线程相互连接时,它们都不能继续工作。让我们考虑以下死锁示例: ```cpp #include std::thread* thr1; std::thread* thr2; void someStuff() {     thr1->join(); } void someAnotherStuff() {     thr2->join(); } int main() {     std::thread t1(someStuff);     std::thread t2(someAnotherStuff);     thr1 = &t1;     thr2 = &t2;     using namespace std::chrono_literals;     std::this_thread::sleep_for(2s);     return 0; } ``` 在主功能中,我们有两个线程,`t1`和`t2`。我们用`something()`函数初始化了`t1`线程,这做了一些有用的工作。我们还用`sometherestuff()`函数初始化了`t2`线程,这做了一些更有用的工作。在由`t2`执行的函数中,我们有指向这些线程的全局指针和指向`t1`线程的连接指针。我们还将指向`t2`线程的指针加入到函数中,该函数由`t1`执行。通过这样做,他们互相结合。这会导致死锁。 在下一节中,我们将考虑用于同步的 C++ 线程库原语以及死锁的另一个原因。 ### 移动多线程闭包的语义 `std::thread`类不可复制,但是如果我们想存储几个线程,或者 10 个或者 20 个呢?当然,我们可以创建线程的数量,然后我们可以像这样连接或分离它们: ```cpp std::thread thr1(someFunc); std::thread thr2(someFunc); std::thread thr3(someFunc); std::thread thr4(someFunc); std::thread thr5(someFunc); thr1.join(); thr2.join(); thr3.join(); thr4.join(); thr5.join(); ``` 但是在 **STL 容器**中存储一堆线程更方便,比如线程的向量: ```cpp std::vector threads; ``` STL 容器不能用于不支持**复制语义**的对象。感谢**移动语义**,我们可以存储具有移动构造函数的不可复制对象,并将赋值操作符移动到容器中。然后,我们可以使用带有`std::move()`函数的线程向量。为了初始化容器中的线程,我们可以做如下事情: ```cpp for (int i = 0; i < 10; i++) {     auto t = std::thread([i]()     {         std::cout << "thread: " << i << "\n";     });     threads.push_back(std::move(t)); } ``` 然后,我们可以加入或分离它们: ```cpp for (auto& thr: threads) {     if (thr.joinable())     {         thr.join();     } } ``` 当我们将`std::thread`对象存储为类成员时,移动语义也很有用。在这种情况下,我们应该仔细设计我们的类,删除复制构造函数和赋值操作符,并实现一个新的移动构造函数和移动赋值操作符。让我们考虑下面这样一个类的代码示例: ```cpp class Handler {     std::thread  threadHandler; public:     Handler(const Handler&) = delete;     Handler& operator=(const Handler&) = delete;     Handler(Handler && obj)     : threadHandler(std::move(obj.threadHandler))     {}     Handler & operator=(Handler && obj)     {         if (threadHandler.joinable())         {             threadHandler.join();         }         threadHandler = std::move(obj.threadHandler);         return *this;     }     ~Handler()     {     if (threadHandler.joinable())         {             threadHandler.join();         }     } }; ``` 在移动赋值操作符中,我们首先检查线程是否可连接。如果是这样,我们加入它,只有在那之后,我们才执行赋值操作。 #### 注意 如果不在线程对象上使用`join()`或`detach()`,我们就不应该将一个线程对象分配给另一个线程对象。这将导致一个`标准::终止()`函数调用。 也可以使用`std::move()`函数将对象移动到线程函数中。对于复制大对象可能有帮助,这是不可取的。让我们执行一个练习来确保对象可以被移动到线程函数中。 ### 练习 3:将对象移动到线程函数 在本练习中,我们将编写一个简单的应用,演示`std::move()`如何为`std::thread`类工作。我们将创建一个同时具有复制构造函数和移动构造函数的类,以查看当我们将这个类的对象移动到`std::thread`函数中时将调用哪个。执行以下步骤完成本练习: 1. 包括支持线程的报头,即 **<线程>** ,以及支持流的报头,即**T4: ```cpp #include #include ```** 2. 实现`处理程序`类,它有默认的构造函数、析构函数、复制构造函数、赋值操作符、移动构造函数和移动赋值操作符。除了打印日志,他们什么都不会做: ```cpp class Handler { public:     Handler()     {         std::cout << "Handler()" << std::endl;     }     Handler(const Handler&)     {         std::cout << "Handler(const Handler&)" << std::endl;     }     Handler& operator=(const Handler&)     {         std::cout << "Handler& operator=(const Handler&)" << std::endl;         return *this;     }     Handler(Handler && obj)     {         std::cout << "Handler(Handler && obj)" << std::endl;     }     Handler & operator=(Handler && obj)     {         std::cout << "Handler & operator=(Handler && obj)" << std::endl;         return *this;     }     ~Handler()     {         std::cout << "~Handler()" << std::endl;     } }; ``` 3. 实现`doSomeJob()`功能,实际上这里什么都不做,只是打印一条日志消息: ```cpp void doSomeJob(Handler&& h) {     std::cout << "I'm here" << std::endl; } ``` 4. 进入`主()`功能,创建`处理程序`类型的`处理程序`变量。创建`thr1`,传递`doSomeJob()`函数,移动处理程序变量: ```cpp Handler handler; std::thread thr1(doSomeJob, std::move(handler)); ``` 5. 分离`thr1`线程,为主线程添加一个小休眠,以避免关闭应用。我们将能够看到分离线程的输出: ```cpp thr1.detach(); using namespace std::chrono_literals; std::this_thread::sleep_for(5s); ``` 6. Run this code in your editor. In the terminal log, from the default constructor, you will see two logs from the move operator, one log from a destructor, a message from the `doSomeJob()` function, and, finally, two other log messages from the destructor. We can see that the move constructor is called twice. 您将获得以下输出: ![](img/C14583_05_16.jpg) ###### 图 5.16:练习执行的结果 可以看到,`处理程序`对象被移动到线程函数中。尽管如此,所有没有使用`std::ref()`函数传递的参数都被复制到了线程的内存中。 让我们考虑一个有趣的问题。大家可能还记得,当我们初始化`std::thread`时,所有的构造函数参数都会被复制到线程内存中,包括一个可调用对象——一个 lambda、一个函数或者一个 std::function。但是如果我们的可调用对象不支持复制语义呢?例如,我们创建了一个只有移动构造函数和移动赋值运算符的类: ```cpp class Converter {     public:     Converter(Converter&&)     {     }     Converter& operator=(Converter&&)     {         return *this;     }     Converter() = default;     Converter(const Converter&) = delete;     Converter& operator=(const Converter&) = delete;     void operator()(const std::string&)     {         // do nothing     } }; ``` 我们如何将它传递给线程构造器?如果我们照原样传递,就会得到一个编译器错误;例如: ```cpp int main() {     Converter convert;     std::thread convertThread(convert, "convert me");     convertThread.join();     return 0; } ``` 您将获得以下输出: ![](img/C14583_05_17.jpg) ###### 图 5.17:编译错误的例子 这里有很多奇怪的错误。要解决这个问题,我们可以使用`std::move()`函数来移动可调用的: ```cpp std::thread convertThread(std::move(convert), "convert me"); ``` 现在,一切都好了——代码已经编译好了,并且完全按照我们想要的方式运行。 现在,让我们考虑一个更有趣的例子。例如,您有一个需要捕捉不可复制对象的 lambda 函数,例如`unique_ptr`: ```cpp auto unique = std::make_unique(); ``` 从 C++ 14 开始,我们可以使用`std::move()`来捕捉可移动对象。因此,要捕获唯一的指针,我们可以使用以下代码: ```cpp std::thread convertThread([ unique = std::move(unique) ] {         unique->operator()("convert me"); }); ``` 如您所见,使用`std::move`捕获 lambda 中的值非常有用。当我们不想复制某些对象时,这也很有用,因为它们可能需要很长时间才能复制。 现在,让我们将我们的知识付诸实践,并编写一个应用示例,演示如何使用线程使用`std::move`。 ### 练习 4:创建和使用线程的 STL 容器 在本练习中,我们将编写一个简单的应用,其中我们将对线程使用`std::move()`。首先,我们将实现一个可移动构造的类。这个类将把小写文本转换成大写文本。然后,我们将创建这个类的实例向量。接下来,我们将创建一个`std::thread`对象的向量。最后,我们将用第一个向量中的一个对象初始化线程。 执行以下步骤完成本练习: 1. 包括用于线程支持的报头,即`<线程>`,流支持,即`< iostream >`,以及`<向量>` : ```cpp #include #include #include #include ``` 2. 实现`转换器`类,它有`m_bufferIn`私有成员`const`T7】STD::vector&T8】类型。这是对小写字符串的原始向量的引用。它还有一个用户构造器,它接受`bufferIn`变量。然后,我们删除复制构造函数和赋值操作符。最后,我们定义重载的`运算符()`,在这里我们将所有小写符号转换为大写。转换后,我们将结果写入结果缓冲区: ```cpp class Converter {     public:     Converter(std::vector& bufferIn)         : m_bufferIn(bufferIn)     {     }     Converter(Converter&& rhs)         : m_bufferIn(std::move(rhs.m_bufferIn))     {     }     Converter(const Converter&) = delete;     Converter& operator=(const Converter&) = delete;     Converter& operator=(Converter&&) = delete;     void operator()(const int idx, std::vector& result)     {         try         {             std::string::const_iterator end = m_bufferIn.at(idx).end();             std::string bufferOut;             for (std::string::const_iterator iter = m_bufferIn.at(idx).begin(); iter != end; iter++)             {                 if (*iter >= 97 && *iter <= 122)                 {                     bufferOut += static_cast(static_cast(*iter) - 32);                 }                 else                 {                     bufferOut += *iter;                 }             }             result[idx] = bufferOut;         }         catch(...)         {             std::cout << "Invalid index" << std::endl;         }     }     private:     const std::vector& m_bufferIn; }; ``` 3. 进入`主()`功能,创建一个名为`numberOfTasks`的常量值,并将其设置为`5`。然后,创建一个`转换器`对象的向量,并用`数量的任务`保留其大小。然后,创建一个`std::thread`对象的向量,并用`numberOfTasks` : ```cpp const int numberOfTasks = 5; std::vector functions; functions.reserve(numberOfTasks); std::vector threads; threads.reserve(numberOfTasks); ``` 保留其大小 4. 创建字符串向量,`textArr`,推五个不同的大字符串进行转换: ```cpp std::vector textArr; textArr.emplace_back("In the previous topics, we learned almost all that we need to work with threads. But we still have something interesting to consider – how to synchronize threads using future results. When we considered condition variables we didn't cover the second type of synchronization with future results. Now it's time to learn that."); textArr.emplace_back("First of all, let's consider a real-life example. Imagine, you just passed the exam at the university. You were asked to wait some amount of time for results. So, you have time to coffee with your mates, and every 10-15 mins you check are results available. Then, when you finished all your other activities, you just come to the door of the lecture room and wait for results."); textArr.emplace_back("In this exercise, we will write a simple application where we will use std::move() with threads. First of all, we will implement a class that is move constructible. This class will convert lowercase text into uppercase text. Then we will create a vector of instances of this class. Next, we will create a vector of std::thread object. Finally, we will initialize threads with an object from the first vector"); textArr.emplace_back("Let's consider one interesting issue. As you remember when we initialize std::thread all constructor arguments are copied into thread memory, including a callable object – lambda, function, std::function. But what if our callable object doesn't support copy semantic? For example, we created a class that has only move constructor and a move assignment operator:"); textArr.emplace_back("Run this code in your editor. You will see in the terminal log from the default constructor, two logs from the move operator, then one log from a destructor, then message from the doSomeJob() function and, finally two other log messages from the destructor. We see that the move constructor is called twice. You will get the output like the following:"); ``` 5. 为循环实现**,我们将**转换器**对象推入函数向量: ```cpp for (int i = 0; i < numberOfTasks; ++ i) {     functions.push_back(Converter(textArr)); } ```** 6. 创建一个字符串的结果向量,并推送五个空字符串。然后,创建一个变量作为数组元素的索引: ```cpp std::vector result; for (int i = 0; i < numberOfTasks; ++ i) {     result.push_back(""); } int idx = 0; ``` 7. 为循环实现另一个**,我们将**标准::线程**对象推入线程向量: ```cpp for (auto iter = functions.begin(); iter != functions.end(); ++ iter) {     std::thread tmp(std::move(*iter), idx, std::ref(result));             threads.push_back(std::move(tmp));     from = to;     to += step; } ```** 8. 为回路实施第三个**,在此我们分离**标准螺纹** : ```cpp for (auto iter = threads.begin(); iter != threads.end(); ++ iter) {      (*iter).detach(); } ```** 9. 为主线程添加一个小休眠,以避免关闭应用。现在,我们可以看到分离线程是如何工作的: ```cpp using namespace std::chrono_literals; std::this_thread::sleep_for(5s); ``` 10. 最后将结果打印到终端: ```cpp for (const auto& str : result) {     std::cout << str; } ``` 11. 在编辑器中运行这段代码。在终端中,您可以看到所有字符串都是大写的,这意味着所有线程都被移动并成功运行。您将获得以下输出: ![Figure 5.18: The result of the exercise’s execution ](img/C14583_05_18.jpg) ###### 图 5.18:练习执行的结果 在本练习中,我们练习了如何创建仅移动对象的 STL 容器。我们还考虑了如何将不可复制的对象传递给线程构造器。这些知识将在下一节帮助我们学习如何从线程中获得结果。 ## 未来、承诺和异步 在前一节中,我们了解了使用线程所需的几乎所有知识。但是我们仍然有一些有趣的事情要考虑,那就是使用未来的结果同步线程。当我们考虑条件变量时,我们没有用未来的结果来覆盖第二种类型的同步。现在,是时候了解一下了。 假设有一种情况,我们运行某个线程并继续其他工作。当我们需要一个结果时,我们停下来检查它是否准备好了。这种情况描述了具有未来结果的实际工作。在 C++ 中,我们有一个名为`<【未来】>`的头文件,其中包含两个表示未来结果的模板类:`std::future < >`和`STD::shared _ future<>`。当我们需要单个未来结果时,我们使用`std::future < >`,当我们需要多个有效副本时,我们使用`STD::shared _ future<>`。我们可以将它们与`std::unique_ptr`和`std::shared_ptr`进行比较。 为了处理未来的结果,我们需要一个特殊的机制来在后台运行任务,并在稍后接收结果:`std::async()`模板函数。它将可调用作为一个参数和启动模式——延迟或异步,当然还有可调用的参数。启动模式`标准::启动::异步`和`标准::启动::延迟`指示如何执行任务。当我们通过`std::launch::async`时,我们期望该函数在单独的线程中执行。当我们通过`STD::launch::delivered`时,函数调用将被延迟,直到我们询问结果。我们也可以同时传递两者,例如`STD::launch::delivered | STD::launch::async`。这意味着运行模式将取决于实现。 现在,让我们考虑一个使用`std::async`的例子`std::future`。我们有一个`to ppercase()`函数,它将给定的字符串转换成大写: ```cpp std::string toUppercase(const std::string& bufIn) {     std::string bufferOut;     for (std::string::const_iterator iter = bufIn.begin(); iter != bufIn.end(); iter++)     {         if (*iter >= 97 && *iter <= 122)         {             bufferOut += static_cast(static_cast(*iter) - 32);         }         else         {             bufferOut += *iter;         }     }     return bufferOut; } ``` 然后,在`main()`函数中,我们创建一个名为`result`的`std::future`变量,并使用`std::async()`返回值对其进行初始化。然后,我们使用结果对象的`get()`函数获取结果: ```cpp #include #include int main() {     std::future result = std::async(toUppercase, "please, make it uppercase");     std::cout << "Main thread isn't locked" << std::endl;     std::cout << "Future result = " << result.get() << std::endl;     return 0; } ``` 实际上,在这里,我们创建了一个未来的对象: ```cpp std::future result = std::async(toUppercase, "please, make it uppercase"); ``` 如您所见,我们没有将启动模式传递给`std::async()`函数,这意味着将使用默认模式:`STD::launch::delivered | STD::launch::async`。您可以明确地这样做: ```cpp std::future result = std::async(std::launch::async, toUppercase, "please, make it uppercase"); ``` 在这里,我们正在等待结果: ```cpp std::cout << "Future result = " << result.get() << std::endl; ``` 如果我们的任务需要很长时间,线程会在这里一直等到结束。 一般来说,我们可以像使用`std::thread`构造函数一样使用`std::async()`函数。我们可以传递任何可调用的对象。默认情况下,所有参数都是复制的,我们可以移动变量和可调用对象,也可以通过引用传递它们。 `std::future`对象不受比赛条件保护。因此,为了从不同的线程访问它并防止损坏,我们应该使用互斥锁。但是如果我们需要共享一个未来的对象,最好用`std::shared_future`。共享的未来结果也不是线程安全的。为了避免竞争情况,我们必须使用互斥体或者在每个线程中存储线程自己的`std::shared_future`副本。 #### 注意 `std::future`对象的比赛条件非常棘手。当线程调用`get()`函数时,未来对象无效。 我们可以通过将未来交给一个建造者来创造一个共享的未来: ```cpp std::future result = std::async(toUppercase, "please, make it uppercase"); std::cout << "Main thread isn't locked" << std::endl; std::shared_future sharedResult(std::move(result)); std::cout << "Future result = " << sharedResult.get() << std::endl; std::shared_future anotherSharedResult(sharedResult); std::cout << "Future result = " << anotherSharedResult.get() << std::endl; ``` 如您所见,我们从`std::future`中创建了一个`std::shared_future`变量并复制了它。两个共享的未来对象指的是同一个结果。 我们还可以使用`sdt::future`对象的`share()`成员函数来创建共享的未来对象: ```cpp std::future result = std::async(toUppercase, "please, make it uppercase"); std::cout << "Main thread isn't locked" << std::endl; auto sharedResult = result.share(); std::cout << "Future result = " << sharedResult.get() << std::endl; ``` 请注意,在这两种情况下,`std::future`对象都将失效。 我们可以从单独的线程获得未来结果的另一种方法是使用`STD::packaged _ task<>`模板类。我们如何与他们合作? 1. 我们创建一个新的`std::packaged_task`并声明可调用函数签名: ```cpp std::packaged_task task(toUppercase); ``` 2. 然后,我们将未来结果存储在`std::future`变量: ```cpp auto futureResult = task.get_future(); ``` 中 3. 接下来,我们在单独的线程中运行这个任务,或者将其作为函数调用: ```cpp std::thread thr1(std::move(task), "please, make it uppercase"); thr1.detach(); ``` 4. Finally, we wait until the future results are ready: ```cpp std::cout << "Future result = " << futureResult.get() << std::endl; ``` #### 注意 `std::packaged_task`不可复制。因此,要在单独的线程中运行它,请使用`std::move()`函数。 还有一件重要的事情需要注意。如果您不希望线程产生任何结果,并且希望等到线程完成工作,可以使用`std::future < void >`。现在,当您调用`future.get()`时,您当前的线程将在此时等待。让我们考虑一个例子: ```cpp #include #include void toUppercase(const std::string& bufIn) {     std::string bufferOut;     for (std::string::const_iterator iter = bufIn.begin(); iter != bufIn.end(); iter++)     {         if (*iter >= 97 && *iter <= 122)         {             bufferOut += static_cast(static_cast(*iter) - 32);         }         else         {             bufferOut += *iter;         }     }     using namespace std::chrono_literals;     std::this_thread::sleep_for(2s);     std::cout << bufferOut << std::endl; } int main() {     std::packaged_task task(toUppercase);     auto futureResult = task.get_future();     std::thread thr1(std::move(task), "please, make it uppercase");     thr1.detach();     std::cout << "Main thread is not blocked here" << std::endl;     futureResult.get();     std::cout << "The packaged_task is done" << std::endl;     return 0; } ``` 如您所见,通过等待另一个线程,我们使用了几种技术,如条件变量、未来结果和承诺。 现在,让我们进入标准库中的下一个重要特性——模板类`std::promise < >`。通过这个类,我们可以设置我们想要接收的类型的值,然后使用`std::future`获取它。我们如何与他们合作?为此,我们需要实现一个带有`标准::承诺`参数的函数: ```cpp void toUppercase(const std::string& bufIn, std::promise result) ``` 工作完成后,我们需要用`std::promise`初始化一个新值: ```cpp result.set_value(bufferOut); ``` 为了在我们将要使用的地方创建`std::promise`,我们需要编写以下代码: ```cpp std::promise stringInUpper; ``` 一旦做到这一点,我们必须创造`std::future`并从承诺中得到它; ```cpp std::future futureRes = stringInUpper.get_future(); ``` 我们需要在单独的线程中运行这个函数: ```cpp std::thread thr(toUppercase, "please, make it uppercase", std::move(stringInUpper)); thr.detach(); ``` 现在,我们需要等到未来确定下来: ```cpp futureRes.wait(); std::cout << "Result = " << futureRes.get() << std::endl; ``` 使用 promises 获得结果的完整示例如下: ```cpp #include #include void toUppercase(const std::string& bufIn, std::promise result) {     std::string bufferOut;     for (std::string::const_iterator iter = bufIn.begin(); iter != bufIn.end(); iter++)     {         if (*iter >= 97 && *iter <= 122)         {             bufferOut += static_cast(static_cast(*iter) - 32);         }         else         {             bufferOut += *iter;         }     }     result.set_value(bufferOut); } int main() {     std::promise stringInUpper;     std::future futureRes = stringInUpper.get_future();     std::thread thr(toUppercase, "please, make it uppercase", std::move(stringInUpper));     thr.detach();     std::cout << "Main thread is not blocked here" << std::endl;     futureRes.wait();     std::cout << "Result = " << futureRes.get() << std::endl;     return 0; } ``` 因此,我们几乎涵盖了编写多线程应用所需的所有内容,除了一件重要的事情——如果在单独的线程中抛出异常会发生什么?例如,您在线程中传递一个函数,它会引发异常。在这种情况下,将为此线程调用`std::terminate()`。其他线程将继续它们的工作。让我们考虑一个简单的例子。 我们有一个`getException()`函数,它生成一个带有线程 ID 的消息,并抛出`std::runtime_error`: ```cpp #include #include #include #include std::string getException() {     std::stringstream ss;     ss << "Exception from thread: ";     ss << std::this_thread::get_id();     throw std::runtime_error(ss.str()); } ``` 我们还有`to ppercase()`功能。该函数将给定的字符串转换为大写,并调用`getException()`函数,该函数引发异常: ```cpp std::string toUppercase(const std::string& bufIn) {     std::string bufferOut;     for (std::string::const_iterator iter = bufIn.begin(); iter != bufIn.end(); iter++)     {         if (*iter >= 97 && *iter <= 122)         {             bufferOut += static_cast(static_cast(*iter) - 32);         }         else         {             bufferOut += *iter;             getException();         }     }     return bufferOut; } ``` 这里是`main()`函数,我们在`试捕`块中创建新线程`thr`。我们捕捉到一个异常,并将消息打印到终端: ```cpp int main() {     try     {         std::thread thr(toUppercase, "please, make it uppercase");         thr.join();     }     catch(const std::exception& ex)     {         std::cout << "Caught an exception: " << ex.what() << std::endl;     }     return 0; } ``` 如果您在 IDE 中运行此代码,您将看到以下输出: ![Figure 5.19: The result of an example’s execution ](img/C14583_05_19.jpg) ###### 图 5.19:一个例子的执行结果 我们可以看到`std::terminate()`在抛出异常后被调用。当程序中有很多线程时,很难找到线程终止的正确位置。幸运的是,我们有一些从另一个线程捕捉异常的机制。让我们把它们都考虑进去。 **std::async** 函数使用将来的结果将异常转移到调用线程。它在将来的结果中存储`标准::异常 _ptr`,并设置就绪标志。然后,当您调用`get()`、 **std::future** 时,会检查是否有任何`std::exception_ptr`存储并重新引发异常。我们所需要做的就是在`试捕`区块中放置一个`get()`调用。让我们考虑一个例子。我们将使用前面例子中的两个辅助函数,即`getException()`和`toUppercase()`。它们将保持不变。在`main()`函数中,我们创建了一个名为`result`的`std::future`对象,并使用`std::async()`函数运行`to ppercase()`函数。然后,我们在`try-catch`块中调用结果对象的`get()`函数,捕捉异常: ```cpp #include #include int main() {     std::future result = std::async(toUppercase, "please, make it uppercase");     try     {         std::cout << "Future result = " << result.get() << std::endl;     }     catch(const std::exception& ex)     {         std::cout << "Caught an exception: " << ex.what() << std::endl;     }     return 0; } ``` 如果您在 IDE 中运行前面的代码,您将获得以下输出: ![Figure 5.20: The result of the example’s execution ](img/C14583_05_20.jpg) ###### 图 5.20:示例执行的结果 如您所见,我们捕捉到了一个异常,现在我们可以通过某种方式处理它。`STD::packaged _ task<>`类以相同的方式处理异常——它将`std::exception_ptr`存储在将来的结果中,设置就绪标志,然后`std::future`在`get()`调用中重新抛出异常。让我们考虑一个小例子。我们将使用前面示例中的两个助手函数- `getException()`和`to ppercase()`。它们将保持不变。在`main()`函数中,我们创建了一个名为`task`的`std::packaged_task`对象。通过使用我们的`to ppercase()`函数的类型,它返回一个整数,并以两个整数作为参数。我们将`传递给`功能到`任务`对象。然后,我们创建一个名为`结果`的`std::future`对象,并使用`get_future()`函数从任务对象中获取结果。最后,我们在新线程`thr`中运行任务对象,并在`try-catch`块中调用`get()`函数的`结果`变量: ```cpp #include #include int main() {     std::packaged_task task(toUppercase);     auto result = task.get_future();     std::thread thr(std::move(task), "please, make it uppercase");     thr.detach();     try     {         std::cout << "Future result = " << result.get() << std::endl;     }     catch(const std::exception& ex)     {         std::cout << "Caught an exception: " << ex.what() << std::endl;     }     return 0; } ``` 如果您在 IDE 中运行此代码,您将获得以下输出: ![Figure 5.21: The result of this example’s execution ](img/C14583_05_21.jpg) ###### 图 5.21:这个例子的执行结果 `std::promise < >`类以另一种方式处理异常。它允许我们使用`set_exception()`或`set _ exception _ at _ thread _ exit()`功能手动存储异常。要在`std::promise`中设置异常,我们必须抓住它。如果我们没有捕捉到异常,将在`std::promise`的析构函数中设置错误为`STD::future _ errc::breaked _ promise`在将来的结果中。当你调用`get()`函数时,会重新抛出一个异常。让我们考虑一个例子。我们将使用上一个示例中的帮助函数–`getException()`。它保持不变。但是我们将`改为`功能,增加第三个参数`std::promise`。现在,我们将调用`中的 **getException()**`**函数,尝试**块,捕捉异常,并将其设置为`std::promise`值: ```cpp void toUppercase(const std::string& bufIn, std::promise result) {     std::string bufferOut;     try     {         for (std::string::const_iterator iter = bufIn.begin(); iter != bufIn.end(); iter++)         {             if (*iter >= 97 && *iter <= 122)             {                     bufferOut += static_cast(static_cast(*iter) - 32);             }             else             {                 bufferOut += *iter;                 getException();             }         }     }     catch(const std::exception& ex)     {         result.set_exception(std::make_exception_ptr(ex));     }     result.set_value(bufferOut); } ``` #### 注意 有几种方法可以打破这个承诺。首先,我们可以捕捉`std::exception`并使用`STD::make _ exception _ ptr()`函数将其转换为`std::exception_ptr`。也可以使用`std::current_exception()`函数,返回`std::exception_ptr`对象。 在`main()`函数中,我们创建了一个名为`upperResult`的整数类型的承诺。我们创建了一个名为`未来`的未来结果,并将其设置为`上结果`承诺值。接下来,我们创建一个新的线程,`thr`,将`传递给它`函数,并将`上移`承诺。然后,我们调用`futureRes`对象的`wait()`函数,使调用线程等待,直到结果可用。然后,在`试捕`块中,我们调用`futureRes`对象的`get()`函数,它重新抛出一个异常: ```cpp #include #include int main() {     std::promise upperResult;     std::future futureRes = upperResult.get_future();     std::thread thr(toUppercase, "please, make it uppercase", std::move(upperResult));     thr.detach();     futureRes.wait();     try     {         std::cout << "Result = " << futureRes.get() << std::endl;     }     catch(...)     {         std::cout << "Caught an exception" << std::endl;     }     return 0; } ``` #### 注意 当我们创建一个`std::promise < >`对象时,我们承诺我们将强制设置该值或异常。如果我们不这样做,`std::promise`的析构函数将抛出一个异常,即`STD::future _ error–STD::future _ errc::breaked _ promise`。 如果您在 IDE 中运行此代码,您将获得以下输出: ![Figure 5.22: The result of this example’s execution ](img/C14583_05_22.jpg) ###### 图 5.22:这个例子的执行结果 这就是多线程应用中处理异常的全部方法。如您所见,这与我们在一个线程中所做的非常相似。现在,让我们将我们的知识付诸实践,并编写一个简单的应用示例,演示如何使用不同的未来结果进行同步。 ### 练习 5:与未来结果同步 在本练习中,我们将编写一个简单的应用来演示如何使用未来的结果从不同的线程接收值。我们将运行`to ppercase()`可调用对象三次。我们将使用`std::async()`函数执行第一个任务,使用`STD::packaged _ task<>`模板类执行第二个任务,使用`std::thread`和`std::promise`执行最后一个任务。 执行以下步骤完成本练习: 1. 包括支持线程的头文件,即`<线程>`,支持流的头文件,即`< iostream >`,支持未来结果的头文件`<>`: ```cpp #include #include #include ``` 2. 实现一个将给定字符串转换为大写的`to ppercase`类。它有两个重载操作符,`(`)。第一个`运算符()`获取要转换的字符串,并以大写形式返回结果值。第二个`运算符()`获取要转换的字符串和一个`std::promise`,并将返回值存储在一个 promise: ```cpp class ToUppercase {     public:     std::string operator()(const std::string& bufIn)     {         std::string bufferOut;         for (std::string::const_iterator iter = bufIn.begin(); iter != bufIn.end(); iter++)         {             if (*iter >= 97 && *iter <= 122)             {                 bufferOut += static_cast(static_cast(*iter) - 32);             }             else             {                 bufferOut += *iter;             }         }         return bufferOut;     }     void operator()(const std::string& bufIn, std::promise result)     {         std::string bufferOut;         for (std::string::const_iterator iter = bufIn.begin(); iter != bufIn.end(); iter++)         {             if (*iter >= 97 && *iter <= 122)             {                 bufferOut += static_cast(static_cast(*iter) - 32);             }             else             {                 bufferOut += *iter;             }         }         result.set_value(bufferOut);     } }; ``` 中 3. 现在,创建一个`to ppercase`对象,即`ptConverter`,并创建一个`std::packaged_task`,即`upper ceresult 1`,该对象以`ptConverter`对象为参数。创建一个`标准::未来`值,并从`上半部分`设置。在单独的线程中运行该任务: ```cpp ToUppercase ptConverter; std::packaged_task upperCaseResult1(ptConverter); std::future futureUpperResult1= upperCaseResult1.get_future(); std::thread thr1(std::move(ptConverter), "This is a string for the first asynchronous task"); thr1.detach(); ``` 4. 现在,创建第二个`来访问`对象,即`转换器`。创建一个名为`futureuperresult 2`的`std::future`对象,并从`std::async()` : ```cpp ToUppercase fConverter; std::future futureUpperResult2 = std::async(fConverter, "This is a string for the asynchronous task"); ``` 进行设置 5. 现在。创建第三个`来访问`对象。即`pConverter`。创建一个名为`承诺结果`的`标准::承诺`值。然后,创建一个名为`future perresult 3`的`std::future`值,并从`promiseResult`中进行设置。现在,在单独的线程中运行`pcconverter`任务,并将`promisereresult`作为参数传递: ```cpp ToUppercase pConverter; std::promise promiseResult; std::future futureUpperResult3 = promiseResult.get_future(); std::thread thr2(pConverter, "This is a string for the task that returns a promise", std::move(promiseResult)); thr2.detach(); ``` 6. 现在,要接收所有线程的结果,请等待`futureperresult 3`准备好,然后获取所有三个结果并打印出来: ```cpp futureUpperResult3.wait(); std::cout  << "Converted strings: "         << futureUpperResult1.get() << std::endl         << futureUpperResult2.get() << std::endl         << futureUpperResult3.get() << std::endl; ``` 7. Run this code in your editor. You will see the converted strings from all three threads. 您将获得以下输出: ![Figure 5.23: The result of this exercise’s execution ](img/C14583_05_23.jpg) ###### 图 5.23:本练习的执行结果 那么,我们在这里做了什么?我们将大型计算拆分成较小的部分,并在不同的线程中运行它们。对于长时间计算,这将提高性能。在本练习中,我们学习了如何从线程接收结果。在本节中,我们还学习了如何将在单独线程中引发的异常传递给调用线程。我们还学习了如何通过一个事件来同步几个线程的工作,不仅用条件变量,而且用未来的结果。 ### 活动 1:创建一个模拟器来模拟美术馆的工作 在本活动中,我们将创建一个模拟器来模拟美术馆的工作。我们设定了参观画廊的人数上限——只能有 50 人进入。为了实现这个模拟,我们需要创建一个`Person`类,它将代表美术馆中的人。另外,我们需要一个`Persons`类,它是一个线程安全的容器。我们还需要一个`守望者`类来控制里面有多少人。如果限制超过了守望员,我们会把所有新来的人都放入等候名单。最后,我们需要一个`生成器`类,它有两个线程——一个用于创建新的访问者,另一个用于通知我们有人必须离开画廊。因此,我们将介绍如何使用线程、互斥体、条件变量、lock_guards 和 unique _ locks。这个模拟器将允许我们利用我们在本章中介绍的技术。因此,在尝试本练习之前,请确保您已经完成了本章前面的所有练习。 为了实现这个应用,我们需要描述我们的类。我们有以下课程: ![Figure 5.24: Description of the classes that are used in this activity ](img/C14583_05_24.jpg) ###### 图 5.24:本活动中使用的类的描述 让我们在开始实现之前创建类图。下图显示了上述所有具有关系的类: ![Figure 5.25: The class diagram ](img/C14583_05_25.jpg) ###### 图 5.25:类图 按照以下步骤实施本活动: 1. 定义并实现 Person 类,该类除了打印日志什么也不做。 2. 为包装 std::vector 类的人员创建一些线程安全存储。 3. 实现 PersonGenerator 类,在不同线程的无限循环中,创建和移除访问者,并通知 Watchman 类。 4. 创建 Watchman 类,在一个独立线程的无限循环中,根据 PersonGenerator 类的通知,将访问者从队列移动到另一个队列。 5. 在 main()函数中声明相应的对象来模拟艺术画廊及其工作方式。 在实现这些步骤之后,您应该会得到以下输出,在这里您可以看到所有实现的类的日志。确保模拟按预期进行。预期的输出应该如下所示: ![Figure 5.26: The result of the application’s execution ](img/C14583_05_26.jpg) ###### 图 5.26:应用执行的结果 #### 注意 这项活动的解决方案可以在第 681 页找到。 ## 总结 在本章中,我们学习了如何使用 C++ 标准库支持的线程。如果我们想要编写健壮、快速和清晰的多线程应用,这是最基本的。 我们首先看一下关于并发性的一般概念——什么是并行、并发、同步、异步和线程执行。对这些概念有一个清晰的理解使我们能够理解多线程应用的架构设计。 接下来,我们研究了开发多线程应用时面临的不同问题,例如数据危险、竞争条件和死锁。了解这些问题有助于我们为项目构建一个清晰的同步架构。我们在一些实际例子中考虑了同步概念,这让我们很好地理解了在编写线程应用时可能面临的挑战。 接下来,我们尝试使用不同的标准库原语进行同步。我们试图弄清楚如何处理竞争条件,并实现了按事件同步和按数据同步的例子。接下来,我们考虑移动语义如何应用于多线程。我们从线程支持库中了解到哪些类是不可复制但可移动的。我们还考虑了移动语义如何在多线程闭包中工作。最后,我们学习了如何从不同的线程接收结果,以及如何使用期货、承诺和异步来同步线程。 我们通过建立一个艺术画廊模拟器将所有这些新技能付诸实践。我们用一个主线程和四个子线程构建了一个多线程应用。我们通过使用条件变量来实现它们之间的通信。我们通过互斥锁使用共享数据来保护它们。总之,我们利用了本章所学的一切。 在下一章中,我们将仔细研究 C++ 中的输入/输出操作和类。我们将从标准库的输入/输出支持开始。然后,我们将继续处理流和异步输入/输出操作。接下来,我们将学习线程和输入/输出的交互。我们将编写一个活动,让我们掌握 C++ 中输入/输出工作的技能。 ================================================ FILE: docs/adv-cpp/07.md ================================================ # 七、流和输入/输出 ## 学习目标 本章结束时,您将能够: * 使用标准输入/输出库向/从文件或控制台写入和读取数据 * 使用内存中的输入/输出接口格式化和解析数据 * 为用户定义的类型扩展标准输入/输出流 * 开发使用多线程输入/输出标准库的应用 在本章中,我们将使用输入/输出标准库开发灵活且可维护的应用,处理流,学习如何在多线程应用中使用输入/输出库,最后学习使用标准库格式化和解析数据。 ## 简介 在前一章中,我们讨论了最具挑战性的话题之一——c++ 中的并发性。我们研究了主要的多线程概念,并区分了 C++ 中的同步、异步和线程执行。我们学习了关于同步、数据危险和比赛条件的要点。最后,我们研究了在现代 C++ 中使用线程。在本章中,我们将深入学习如何在多线程应用中处理输入/输出。 本章专门介绍 C++ 中的`流`和`I/O`。输入输出是输入输出操作的一般概念。标准库这一部分的主要目的是提供一个关于数据输入和输出的清晰界面。但这不是唯一的目标。在许多情况下,输入/输出可以帮助我们的应用。很难想象任何应用不把错误或异常情况写入日志文件,目的是把它发送给开发团队进行分析。在图形用户界面应用中,我们总是需要格式化显示的信息或解析用户输入。在复杂的大型应用中,我们通常需要记录内部数据结构,等等。在所有这些情况下,我们使用`标准库`的输入/输出部分。 我们将以对标准库的输入/输出部分的简单介绍开始本章。我们将了解输入/输出,并探索它们的主要概念和术语。然后,我们将考虑默认支持哪些类型,以及如何将流扩展到用户定义的类型。接下来,我们将研究输入/输出库的结构,并检查可供我们使用的头和类。最后,我们将研究如何处理流、读写文件、创建具有输入和输出操作的多线程应用,以及格式化和解析文本数据。 本章将以一个富有挑战性和激动人心的活动结束,在这个活动中,我们将改进上一章中的`美术馆模拟器`项目,并创建一个健壮、清晰、多线程且易于使用的`记录器`。我们将开发一个接口清晰的类,可以从项目中的任何地方访问。接下来,我们将对它进行调整,使其能够与几个线程一起工作。最后,我们将把我们健壮的记录器集成到艺术画廊模拟器项目中。 让我们从查看 C++ 标准库的输入/输出部分开始,了解这套工具为我们提供了哪些机会。 ### 查看标准库的输入/输出部分 在计算机科学中,输入/输出这个术语意味着程序、设备、计算机等之间的通信。在 C++ 中,我们使用标准输入和标准输出术语来描述输入/输出过程。标准输入是指传输到程序中的数据流。为了获得这些数据,程序应该执行读取操作。标准输出是指从程序传输到外部设备的数据流,如文件、显示器、套接字、打印机等。为了输出这些数据,程序应该执行写操作。标准输入和输出流从主进程继承而来,对所有子线程都是通用的。请看下图,以更好地理解所考虑的术语: ![](img/C14583_06_01.jpg) ###### 图 6.1:设备之间的输入/输出通信 在 C++ 标准库中,大多数输入输出类都是通用的类模板。所有这些逻辑上都分为两类——抽象和实现。我们已经熟悉了抽象类,并且知道我们可以在不重新编译代码的情况下将它们用于不同的目的。输入/输出库也是如此。这里,我们有六个抽象类,它们是 C++ 中输入输出操作的基础。我们不会深入探讨这些接口。通常,我们在操作中使用更多的高级类,只有当我们需要实现自己的派生类时,才会对它们有吸引力。 **ios_base** 抽象类负责管理流状态标志、格式化标志、回调和私有存储。 **basic_streambuf** 抽象类提供了缓冲输入或输出操作的接口,并提供了对输入源的访问,如文件、套接字或输出接收器,如字符串或向量。 **basic_ios** 抽象类实现了从 **basic_streambuf** 接口使用派生类的工具。 **basic_ostream** 、 **basic_istream** 、 **basic_iostream** 抽象类分别是来自 **basic_streambuf** 接口的派生类的包装器,提供高级输入输出接口。让我们简单考虑一下它们及其关系,如下类图所示。可以看到除了 **ios_base** 之外,都是模板类。在每个类的名称下,您可以找到定义该类的文件名: #### 注意 在 UML 符号中,我们使用`< <接口> >`关键字来表示类是一个抽象类。 ![Figure 6.2: Class diagram of I/O abstract interfaces ](img/C14583_06_02.jpg) ###### 图 6.2:输入输出抽象接口类图 实现类在逻辑上分为以下几类:**文件 I/O** 、**字符串 I/O** 、**同步 I/O** 、 **I/O 操纵器**,以及预定义的标准流对象。它们都是从前面提到的抽象类中派生出来的。让我们在接下来的章节中详细考虑它们。 ### 预定义的标准流对象 我们将从已经熟悉的`< iostream >`头文件中的`std::cout`类开始了解输入/输出标准库。我们用它向终端输出数据。您可能还知道用于读取用户输入的`std::cin`类,但不是每个人都知道`std::cout`和`std::cin`是预定义的标准流对象,用于格式化终端的输入和输出。< iostream >头文件还包含`std::cerr`和`std::clog`流对象,用于记录错误。和往常一样,宽字符也有它们的类似物,前缀为“`w`”:`wcout`、`wcin`、`wcerr`、`wclog`。所有这些对象都会在系统启动时自动创建和初始化。虽然从多个线程使用这些对象是安全的,但是输出可以是混合的。让我们修改如何使用它们。因为它们只为内置类型重载,所以我们应该为用户定义的类型编写自己的重写。 `标准::cout`流对象通常与`标准::endl`操纵器一起使用。它在输出序列中插入一个换行符并刷新它。下面是一个使用它们的例子: ```cpp std::string name("Marilyn Monroe"); int age = 18; std::cout << "Name: " << name << ", age: " << age << std::endl; ``` 最初,`std::cin`对象逐符号读取所有输入字符序列。但是它有内置类型的重载,可以读取诸如`数字`、`字符串`、`字符`等值。读弦有一个小技巧;`std::cin`读取字符串,直到下一个空白或换行符。所以,如果需要它读字符串,就要循环进行,一个字一个字读,或者使用`std::getline()`函数,该函数以`std::cin`对象为第一个参数,目的字符串为第二个参数。 #### 注意 `标准::cin`流对象的右移位运算符`> >`仅读取一行中的一个单词。使用`std::getline(std::cin,str)`读取整行。 这里有一个使用不同类型的`std::cin`的例子; ```cpp std::string name; std::string sex; int age; std::cout << "Enter your name: " << std::endl; std::getline(std::cin, name); std::cout << "Enter your age: " << std::endl; std::cin >> age; std::cout << "Enter your sex (male, female):" << std::endl; std::cin >> sex; std::cout << "Your name is " << name << ", your age is " << age << ", your sex is " << sex << std::endl; ``` 如您所见,在这里,我们使用`std::getline()`函数读取名称,因为用户可以输入两三个单词。我们还使用右移位运算符、`> >`来读取年龄,然后读取性别,因为我们只需要读取一个单词。然后,我们打印读取的数据,以确保一切顺利。 **std::cerr** 和 **std::clog** 流对象仅在一个方面不同–**STD::cerr**会立即刷新输出序列,而 **std::clog** 会对其进行缓冲,并仅在缓冲区已满时进行刷新。说到用法,跟 **std::cout** 很像。唯一不同的是来自 **std::cerr** 和 **std::clog** 的消息(在大多数 IdE 中)是红色的。 在下面的截图中,您可以看到这些流对象的输出: ![](img/C14583_06_03.jpg) ###### 图 6.3:标准::cerr 和标准::clog 流对象的输出 现在,让我们做一个练习来巩固我们所学的一切。 ### 练习 1:覆盖用户定义类型的左移位运算符< < 在本练习中,我们将编写一段非常有用的代码,您可以在任何地方使用它来输出用户定义的类型。首先,我们将创建一个名为`Track`的类,代表一个音乐曲目。它将有以下私人成员:`名字`、`歌手`、`长度`和`日期`。然后,我们将覆盖左移位运算符,`< <`,对于这个类。接下来,我们将创建这个类的一个实例,并使用`std::cout`流对象输出它。 执行以下步骤来执行本练习: 1. 包括用于输出到控制台的所需标题: **< iostream >** 和用于字符串支持的字符串<>: ```cpp #include #include ``` 2. 声明`轨道`类,并添加私有节变量来保存`轨道`的信息,即`m_Name`、`m_Singer`、`m_Date`和`m_LengthInSeconds`。在公共部分,添加一个构造函数,其参数初始化所有私有变量。另外,为所有类成员添加`公共`区域获取器: ```cpp class Track { public:      Track(const std::string& name,            const std::string& singer,            const std::string& date,            const unsigned int& lengthInSeconds)            : m_Name(name)            , m_Singer(singer)            , m_Date(date)            , m_LengthInSeconds(lengthInSeconds) { }      std::string getName() const { return m_Name; }      std::string getSinger() const { return m_Singer; }      std::string getDate() const { return m_Date; }      unsigned int getLength() const { return m_LengthInSeconds; } private:      std::string m_Name;      std::string m_Singer;      std::string m_Date;      unsigned int m_LengthInSeconds; }; ``` 3. 现在是练习中最困难的部分:为`轨道`类型编写重载函数。这是一个`模板`函数,有两个类型参数:`图表`和`特征` : ```cpp template ``` 4. 我们内联了这个函数,让编译器知道我们希望它对这个函数进行优化。这个函数的返回类型是对一个`std::basic_ostream < charT,Traits >`类的引用。这个函数的名字是`< <`运算符。该函数采用两个参数:第一个是对`STD::basic _ ostream`类的引用,第二个是`Track`变量的副本。完整的功能声明如下: ```cpp template inline std::basic_ostream& operator<<(std::basic_ostream& os, Track trackItem); ``` 5. 现在,添加函数定义。使用`os`变量,就像我们使用`std::cout`对象一样,按照您的意愿格式化输出。然后,从函数中返回`os`变量。重载运算符`< <`的完整代码如下: ```cpp template inline std::basic_ostream& operator<<(std::basic_ostream& os, Track trackItem) {       os << "Track information: ["          << "Name: " << trackItem.getName()          << ", Singer: " << trackItem.getSinger()          << ", Date of creation: " << trackItem.getDate()          << ", Length in seconds: " << trackItem.getLength()          << "]";       return os; } ``` 6. 现在,进入`主`功能,创建并初始化名称为`track_001`的`Track`类型的实例。最后,使用`std::cout`打印`track_001`值: ```cpp int main() {      Track track_001("Summer night city",                      "ABBA",                      "1979",                       213);      std::cout << track_001 << std::endl;      return 0; } ``` 7. 编译并执行应用。运行它。您将获得以下输出: ![](img/C14583_06_04.jpg) ###### 图 6.4:执行练习 1 的结果 干得好。在这里,我们考虑使用预定义的标准流对象,并学习了如何为用户定义的类型编写自己的重载移位运算符。让我们继续,用 C++ 标准 IO 库检查对文件的读写。 ### 文件输入输出实现类 文件流管理文件的输入和输出。它们提供了一个实现**资源获取是初始化** ( **RAII** )的接口——文件在构建流时打开,在销毁时自动关闭。在标准库中,文件流由以下类表示:`basic_ifstream`用于输入操作,`basic_ofstream`用于输出操作,`basic _ fsstream`用于输入和输出操作,以及`basic_filebuf`用于原始文件设备的实现。所有这些都在`<流>`头文件中定义。标准库还为 char 和`wchar_t`类型提供了类型定义,即`ifstream`、`fsstream`和`ofstream`,宽字符的名称前缀为“`w`”。 我们可以通过两种方式创建文件流。第一种方法是在一行中完成,也就是说,只需将文件名传递给构造函数,就可以打开文件并将流连接到文件: ```cpp std::ofstream outFile(filename); std::ifstream outFile(filename); std::fstream outFile(filename); ``` 另一种方法是创建一个对象,然后调用`open()`函数: ```cpp std::ofstream outFile; outFile.open(filename); ``` #### 注意 IO 流有布尔变量:一个**好位**、一个**坏位**、一个**坏位**和一个**坏位**。它们用于在每次操作后检查流的状态,并指示流中发生了哪个错误。 对象创建后,我们可以通过检查`故障位`或检查与打开文件相关联的流来检查流状态。要检查一个`故障位`,调用`文件`流上的`故障()`功能: ```cpp if (outFile.fail()) {     std::cerr << filename << " file couldn't be opened"<< std::endl; } ``` 要检查流是否与打开的文件相关联,请调用`is_open()`函数: ```cpp if (!outFile.is_open()) {     std::cerr << filename << " file couldn't be opened"<< std::endl; } ``` 输入、输出和双向文件流也可以通过使用标志以不同的模式打开。它们在`ios_base`命名空间中声明。除了`ios_base::in`和`ios_base::out`标志外,我们还有`ios_base::ate`、`ios_base::app`、`ios_base::trunc`和`ios_base::binary`标志。`ios_base::trunc`标志删除文件内容。`ios_base::app`标志始终将输出写入文件末尾。即使您决定更改文件中的位置,也不能这样做。`ios_base::ate`标志将文件描述符的位置设置到文件的末尾,但允许您稍后修改位置。最后,`ios_base::binary`标志抑制数据的任何格式,以便以“原始”格式读取或写入数据。让我们考虑开放模式的所有可能组合。 默认情况下, **std::ofstream** 在 **ios_base::out** 模式下打开, **std::ifstream** 在 **ios_base::in** 模式下打开,**STD::fsstream**在**IOs _ base::in | IOs _ base::out**模式下打开。如果文件不存在,则**IOs _ base::out | IOs _ base::trunc**模式会创建该文件,或者从现有文件中删除所有内容。**IOs _ base::out | IOs _ base::app**模式会在文件不存在的情况下创建文件,或者打开现有文件,只允许在文件末尾写入。上述两种模式都可以与标志中的 **ios_base::结合使用,因此文件将以读写模式同时打开。** 以下是如何使用上述模式打开文件的示例: ```cpp std::ofstream outFile(filename, std::ios_base::out|std::ios_base::trunc); ``` 您还可以执行以下操作: ```cpp std::ofstream outFile; outFile.open(filename, std::ios_base::out|std::ios_base::trunc); ``` 在以所需模式打开文件流后,我们可以开始读取或写入文件。文件流允许我们改变文件中的位置。让我们考虑如何做到这一点。要获取当前文件的位置,我们可以在 **ios_base::out** 模式下调用 **tellp()** 函数,在模式下调用**IOs _ base::out**tellg()**函数。它可以在以后使用,以便我们在需要时可以回到这个位置。在 **ios_base::out** 模式下使用 **seekp()** 功能,在 **ios_base::in** 模式下使用 **seekg()** 功能,也可以找到文件中的确切位置。它需要两个参数:要移动的字符数和应该从哪个文件位置开始计数。**查找允许三种位置:std::ios_base::beg** 即文件开头, **std::ios_base::end** 即文件结尾, **std::ios_base::cur** 即当前位置。下面是调用 **seekp()** 函数的一个例子:** ```cpp outFile.seekp(-5, std::ios_base::end); ``` 如您所见,我们要求将当前文件的位置设置在文件末尾的第五个字符处。 要写入文件,我们可以使用重载的左移位运算符,`< <`,对于一般的格式化输出,`put()`函数写单个字符,或者`write()`函数写一个字符块。使用左移位运算符是将数据写入文件的最方便的方法,因为您可以将任何内置类型作为参数传递: ```cpp outFile << "This is line No " << 1 << std::endl; ``` `put()`和`write()`函数只能用于字符值。 要读取文件,我们可以使用重载的右移位运算符,`> >`,或者使用一组读取字符的函数,如`read()`、`get()`、`getline()`。右移位运算符对于所有内置类型都是重载的,我们可以这样使用它: ```cpp std::ifstream inFile(filename); std::string str; int num; float floatNum; // for data: "book 3 24.5" inFile >> str >> num >> floatNum; ``` 最后,当执行离开可见性范围时,文件流被关闭,因此我们不需要执行任何额外的操作来关闭文件。 #### 注意 从文件中读取数据时要注意。右移位运算符`> >`,只读取一个字符串,直到出现一个空白或换行符。要读取完整的字符串,您可以使用循环或在单独的变量中读取每个单词,就像我们在*练习 1* 、*中为用户定义的类型*覆盖左移位运算符<、<一样。 现在,让我们练习使用 C++ IO 标准库向文件读写数据。 ### 练习 2:向文件中读写用户定义的数据类型 在本练习中,我们将为书店编写一段代码。我们需要将图书价格信息存储在一个文件中,然后在需要时从文件中读取这些信息。为了实现这一点,我们将创建一个类,该类表示一本书的名称、作者、出版年份和价格。接下来,我们将创建这个类的一个实例,并将它写入一个文件。稍后,我们将从文件中读取有关书籍的信息,并将其导入书籍类的实例中。执行以下步骤完成本练习: 1. 包括所需的头文件:`< iostream >`用于输出到控制台,`< string >`用于字符串支持,``用于 I/O 文件库支持: ```cpp #include #include #include ``` 2. 实现`Book`类,代表一个书店里的书。在私有部分,用不言自明的名称定义四个变量:`m_Name`、`m_Author`、`m_Year`、`m_Price`。在公共部分,定义一个带有参数的构造函数,该构造函数初始化所有类成员。此外,在`公共`部分,定义所有类成员的获取者: ```cpp class Book { public:       Book(const std::string& name,            const std::string& author,            const int year,            const float price)      : m_Name(name)      , m_Author(author)      , m_Year(year)      , m_Price(price) {}      std::string getName() const { return m_Name; }      std::string getAuthor() const { return m_Author; }      int getYear() const { return m_Year; }      float getPrice() const { return m_Price; } private:      std::string m_Name;      std::string m_Author;      int m_Year;      float m_Price; }; ``` 3. 进入`主`功能,声明`价格文件`变量,保存文件名: ```cpp std::string pricesFile("prices.txt"); ``` 4. 接下来,创建`图书`类的实例,并使用`图书名称`、`作者名称`、`年份`和`价格` : ```cpp Book book_001("Brave", "Olena Lizina", 2017, 33.57); ``` 对其进行初始化 5. 将此类实例写入文件。创建`std::ofstream`类的实例。用`价格文件`变量名打开我们的文件。检查流是否打开成功,如果没有打开,打印错误信息: ```cpp std::ofstream outFile(pricesFile); if (outFile.fail()) {       std::cerr << "Failed to open file " << pricesFile << std::endl;       return 1; } ``` 6. 然后,使用 getters 将所有关于`book_001`的信息写入文件,每个项目之间有空格,末尾有一个换行符: ```cpp outFile << book_001.getName() << " "         << book_001.getAuthor() << " "         << book_001.getYear() << " "         << book_001.getPrice() << std::endl; ``` 7. Compile and execute the application. Now, go to the project folder and find where the '**prices.txt**' file is located. In the following screenshot, you can see the location of the created file in the project directory: ![](img/C14583_06_05.jpg) ###### 图 6.5:创建的文件的位置 8. Open it in **Notepad**. In the following screenshot, you can see what the output to the file looks like: ![](img/C14583_06_06.jpg) ###### 图 6.6:用户定义类型输出到文件的结果 9. 现在,让我们将这些数据读入变量。创建`std::ifstream`类的实例。打开文件`价格文件`。检查流是否已成功打开,如果未打开,则打印错误消息: ```cpp std::ifstream inFile(pricesFile); if (inFile.fail()) {      std::cerr << "Failed to open file " << pricesFile << std::endl;      return 1; } ``` 10. 从文件中创建用于输入的局部变量,即`名称`、`作者名称`、`作者姓氏`、`年份`和`价格`。他们的名字不言自明: ```cpp std::string name; std::string authorName; std::string authorSurname; int year; float price; ``` 11. 现在,按照文件中的顺序将文件中的数据读入变量: ```cpp inFile >> name >> authorName >> authorSurname >> year >> price; ``` 12. 创建一个名为`book_002`的`Book`实例,并用那些读取的值初始化它: ```cpp Book book_002(name, std::string(authorName + " " + authorSurname), year, price); ``` 13. 要检查读取操作是否成功执行,请将`book_002`变量打印到控制台: ```cpp std::cout  << "Book name: " << book_002.getName() << std::endl            << "Author name: " << book_002.getAuthor() << std::endl            << "Year: " << book_002.getYear() << std::endl            << "Price: " << book_002.getPrice() << std::endl; ``` 14. 再次编译并执行应用。在控制台中,您将看到以下输出: ![](img/C14583_06_07.jpg) ###### 图 6.7:执行练习 2 的结果 如您所见,我们毫无困难地从文件中写入和读取自定义格式的数据。我们创建了自己的自定义类型,使用`std::ofstream`类将其写入文件,并检查是否一切都写成功。然后,我们使用`std::ifstream`类将这些数据从一个文件读取到我们的自定义变量中,将其输出到控制台,并确保所有内容都被正确读取。通过这样做,我们学习了如何使用输入/输出标准库向文件读写数据。现在,让我们继续学习输入/输出库的内存部分。 ### 字符串输入/输出实现 输入/输出标准库不仅允许输入和输出到文件等设备,还允许输入和输出到内存,特别是输入和输出到`标准::字符串`对象。在这种情况下,字符串可以是输入操作的源,也可以是输出操作的接收器。在`<流>`头文件中,声明了管理字符串输入和输出的流类。它们和文件流一样,也提供了一个实现 RAII 的接口——字符串在创建流时打开进行读取或写入,在销毁流时关闭。它们在标准库中由以下类表示:`basic_stringbuf`,它实现了一个原始字符串接口,`basic_istringstream`用于输入操作,`basic_ostringstream`用于输出操作,`basic_stringstream`用于输入和输出操作。标准库还为`char`和`wchar_t`类型提供了类型定义:`stream`、`ostringstream`、`stringstream`以及宽字符前缀为“w”的相同名称。 要创建`STD::is tingstream`类的对象,我们应该将初始值设定项字符串作为构造函数参数传递,或者稍后使用`str()`函数进行设置: ```cpp std::string track("ABBA 1967 Vule"); std::istringstream iss(track); ``` 或者,我们可以执行以下操作: ```cpp std::string track("ABBA 1967 Vule"); std::istringstream iss; iss.str(track); ``` 接下来,要从流中读取值,请使用右移位运算符,`> >`,该运算符对所有内置类型都是重载的: ```cpp std::string group; std::string name; int year; iss >> group >> year >> name; ``` 要创建`std::ostringstream`类的对象,我们只需声明其类型的变量: ```cpp std::ostringstream oss; ``` 接下来,要将数据写入字符串,请使用左移位运算符,`< <`,该运算符对所有内置类型都是重载的: ```cpp std::string group("ABBA"); std::string name("Vule"); int year = 1967; oss << group << std::endl     << name << std::endl     << year << std::endl; ``` 要获取结果字符串,请使用`str()`函数: ```cpp std::cout << oss.str(); ``` `std::stringstream`对象是双向的,因此它既有默认构造函数,也有接受字符串的构造函数。我们可以通过声明这种类型的变量来创建默认的`std::stringstream`对象,然后将其用于读写; ```cpp std::stringstream ss; ss << "45"; int count; ss >> count; ``` 另外,我们可以使用带有字符串参数的构造函数创建`std::stringstream`。然后,我们可以像往常一样使用它进行阅读和写作: ```cpp std::string employee("Alex Ismailow 26"); std::stringstream ss(employee); ``` 或者,我们可以创建一个默认的`std::stringstream`对象,并通过使用`str()`函数设置一个字符串来初始化它: ```cpp std::string employee("Charlz Buttler 26"); std::stringstream ss; ss.str(employee); ``` 接下来,我们可以使用 ss 对象进行读写: ```cpp std::string name; std::string surname; int age; ss >> name >> surname >> age; ``` 我们也可以为这些类型的流应用开放模式。它们的功能类似于文件流,但略有不同。`ios_base::binary`在处理字符串流的情况下是不相关的,`ios_base::trunc`被忽略。因此,我们可以在四种模式下打开任意字符串流:`ios_base::app`、`ios_base::ate`和`IOs _ base::in/IOs _ base::out`。 现在,让我们练习使用 C++ IO 标准库向字符串读写数据。 ### 练习 3:为字符串中的替换单词创建函数 在本练习中,我们将实现一个函数,该函数解析给定的字符串并用其他单词替换给定的单词。为了完成这个练习,我们创建了一个可调用的类,该类接受三个参数:原始字符串、要替换的单词和将用于替换的单词。因此,应该返回新字符串。执行以下步骤完成本练习: 1. 包括输出到终端的必要标题:`<输出流>`和输入/输出字符串支持的输出流<>T4: ```cpp #include #include ``` 2. 实现名为`Replacer`的可调用类。它只有一个函数——一个重载的圆括号运算符,(),它返回一个字符串并接受三个参数:原始字符串、要替换的单词和要用于替换的单词。函数声明如下: ```cpp std::string operator()(const std::string& originalString,                        const std::string& wordToBeReplaced,                        const std::string& wordReplaceBy); ``` 3. 接下来,创建`isting stream`对象,即`iss`,并将`originalString`变量设置为输入源: ```cpp std::istringstream iss(originalString); ``` 4. 创建`排斥流`对象,即`oss`,它将保存转换后的字符串: ```cpp std::ostringstream oss; ``` 5. 然后,在循环中,当有可能的输入时,执行对单词变量的单词读取。检查这个单词是否等于**单词被替换**变量。如果是,用变量替换它,并写入 **oss** 流。如果不相等,将原字写到 **oss** 流。在每个单词之后,添加一个空白字符,因为 **iss** 流会截断它们。最后,返回结果。完整的类如下: ```cpp class Replacer { public:       std::string operator()(const std::string& originalString,                              const std::string& wordToBeReplaced,                              const std::string& wordReplaceBy)      {            std::istringstream iss(originalString);            std::ostringstream oss;            std::string word;            while (iss >> word)            {                 if (0 == word.compare(wordToBeReplaced))                 {                      oss << wordReplaceBy << " ";                 }                 else                 {                      oss << word << " ";                 }            }            return oss.str();      } }; ``` 6. 进入`主`功能。创建一个名为 worker 的`Replacer`类的实例。定义`foodList`变量,用包含食物列表的字符串初始化;有些项目应该重复。定义`changedList`字符串变量,并通过`worker()`函数的返回值对其进行初始化。使用`标准::cout`在终端显示结果: ```cpp int main() {       Replacer worker;       std::string foodList("coffee tomatoes coffee cucumbers sugar");       std::string changedList(worker(foodList, "coffee", "chocolate"));       std::cout << changedList;       return 0; } ``` 7. 编译、构建和运行练习。因此,您将获得以下输出: ![Figure 6.8: The result of executing Exercise 3](img/C14583_06_08.jpg) ###### 图 6.8:执行练习 3 的结果 干得好!在这里,我们学习了如何使用字符串流来格式化输入和输出。我们创建了一个应用,可以轻松替换句子中的单词,增强了我们的知识,现在我们准备学习输入/输出操纵器,这样我们就可以提高处理线程的技能。 ### 输入/输出操纵器 到目前为止,我们已经了解了使用流的简单输入和输出,但是在许多情况下它们还不够。对于更复杂的输入/输出数据格式化,标准库有一大套输入/输出操纵器。它们是开发出来与左(<>)移位运算符一起控制流行为的函数。输入/输出操纵器分为两种类型——无参数调用的和需要参数的。其中一些既用于输入又用于输出。让我们简单考虑一下它们的含义和用法。 ### 用于更改流的数值基数的输入/输出操纵器 在`< ios >`头中,有用于更改流的数字基数的声明函数:`std::dec`、`std::hex`和`std::oct`。它们在没有参数的情况下被调用,并将流的基数分别设置为十进制、十六进制和八进制。在`< iomanip >`头中,声明了`std::setbase`函数,使用以下参数调用:8、10 和 16。它们可以互换,用于输入和输出操作。 在`< ios >`头中,还有`std::showbase`和`std::noshowbase`功能,控制显示流的数字基数。它们只影响十六进制和八进制整数输出,除了零值,以及货币输入和输出操作。让我们完成一个练习,并学习如何在实践中使用它们。 ### 练习 4:以不同的数字基数显示输入的数字 在本练习中,我们将开发一个应用,该应用在无限循环中要求用户以下列数字基数之一输入一个整数:十进制、十六进制或八进制。读取输入后,它会以其他数字表示形式显示该整数。要执行本练习,请完成以下步骤: 1. 包括用于流支持的`< iostream >`报头。声明名为`BASE`的枚举,定义三个值:`DECIMAL`、`OCTAL`、`十六进制` : ```cpp #include enum BASE {       DECIMAL,       OCTAL,       HEXADECIMAL }; ``` 2. 声明一个名为`displayInBases`的函数,该函数接受两个参数——整数和基数。接下来,定义 switch 语句,该语句测试接收到的数字基数,并以另外两种数字表示形式显示给定的整数: ```cpp void displayInBases(const int number, const BASE numberBase) {   switch(numberBase)   {   case DECIMAL:     std::cout << "Your input in octal with base: "           << std::showbase << std::oct << number           << ", without base: "           << std::noshowbase << std::oct << number << std::endl;     std::cout << "Your input in hexadecimal with base: "           << std::showbase << std::hex << number           << ", without base: "           << std::noshowbase << std::hex << number << std::endl;     break;   case OCTAL:     std::cout << "Your input in hexadecimal with base: "           << std::showbase << std::hex << number           << ", without base: "           << std::noshowbase << std::hex << number << std::endl;     std::cout << "Your input in decimal with base: "           << std::showbase << std::dec << number           << ", without base: "           << std::noshowbase << std::dec << number << std::endl;     break;   case HEXADECIMAL:     std::cout << "Your input in octal with base: "           << std::showbase << std::oct << number           << ", without base: "           << std::noshowbase << std::oct << number << std::endl;     std::cout << "Your input in decimal with base: "           << std::showbase << std::dec << number           << ", without base: "           << std::noshowbase << std::dec << number << std::endl;     break;   } } ``` 3. 进入`主`功能,定义用于读取用户输入的整数变量: ```cpp int integer; ``` 4. 创建一个无限 while 循环。在循环中,要求用户输入一个十进制值。将输入读取为十进制整数。传递给`显示基站`功能。接下来,要求用户输入一个十六进制值。将输入读取为十六进制整数。传递到`显示基站`功能。最后,要求用户输入一个八进制值。将输入读取为八进制整数。传递到`显示框`功能: ```cpp int main(int argc, char **argv) {   int integer;   while(true)   {     std::cout << "Enter the decimal value: ";     std::cin >> std::dec >> integer;     displayInBases(integer, BASE::DECIMAL);     std::cout << "Enter the hexadecimal value: ";     std::cin >> std::hex >> integer;     displayInBases(integer, BASE::HEXADECIMAL);     std::cout << "Enter the octal value: ";     std::cin >> std::oct >> integer;     displayInBases(integer, BASE::OCTAL);   }   return 0; } ``` 5. Build and run the application. Follow the output and enter, for example, 12 in different numeric representations. The output should be as follows: ![Figure 6.9: The result of executing Exercise 4, part 1](img/C14583_06_09.jpg) ###### 图 6.9:执行练习 4 第 1 部分的结果 6. 现在,让我们在`std::setbase()`功能中更改`std::dec`、`std::oct`和`std::hex`,检查输出是否相同。首先,为`std::setbase()`支持添加`< iomanip >`头。接下来,在循环中的主功能中,将`std::dec`替换为`std::setbase(10)`、`std::hex`替换为`std::setbase(16)`,将`std::oct`替换为`std::setbase(8)` : ```cpp int main(int argc, char **argv) {   int integer;   while(true)   {     std::cout << "Enter the decimal value: ";     std::cin >> std::setbase(10) >> integer;     displayInBases(integer, BASE::DECIMAL);     std::cout << "Enter the hexadecimal value: ";     std::cin >> std::setbase(16) >> integer;     displayInBases(integer, BASE::HEXADECIMAL);     std::cout << "Enter the octal value: ";     std::cin >> std::setbase(8) >> integer;     displayInBases(integer, BASE::OCTAL);   }   return 0; } ``` 7. 再次,构建并运行应用。根据输出,在不同的数字表示中输入相同的整数(12)。输出应如下所示: ![Figure 6.10: The result of executing Exercise 4, part 2](img/C14583_06_10.jpg) ###### 图 6.10:执行练习 4 第 2 部分的结果 现在,比较结果。如您所见,输出是相同的。通过这样做,我们确保了这些功能是可互换的。 ### 浮点格式的输入/输出操纵器 在`< ios >`表头,有声明的改变浮点数字格式的函数:`std::fixed`、`std::scientific`、`std::hexfloat`、`std::defaultfloat`。它们在没有参数的情况下被调用,并将`浮动字段`分别设置为固定值、科学值、固定值和科学值以及默认值。还有`std:: showpoint`和`std::noshowpoint`功能,控制显示浮点数字。它们只影响产量。`std::noshowpoint`函数只影响没有小数部分的浮点数字。 在`< iomanip >`头中,有一个声明的`std:: setprecision`函数,用一个代表精度的数字调用。当点右侧的数字被删除时,结果将被舍入。如果数字太大,无法以正常方式表示,则忽略精度规格,以更方便的方式显示数字。您只需要设置一次精度,并在需要另一个精度时更改它。当您选择存储浮点变量的数据类型时,您应该注意到一些技巧。在 C++ 中,有三种数据类型可以表示浮点值:浮点、双精度和长双精度。 浮点通常是 4 字节,双精度是 8 字节,长双精度是 8、12 或 16 字节。所以,每种方法的精确度都是有限的。浮点型最多可容纳 6-9 个有效数字,双精度型最多可容纳 15-18 个有效数字,长双精度型最多可容纳 33-36 个有效数字。如果您想比较它们之间的差异,请查看下表: ![Figure 6.11: Comparison table of the floating-point types](img/C14583_06_11.jpg) ###### 图 6.11:浮点类型对照表 #### 注意 当你需要超过六个有效数字的精度时,请选择双精度,否则你会得到意想不到的结果。 让我们完成一个练习,并学习如何在实践中使用它们。 ### 练习 5:以不同格式显示输入的浮点数 在本练习中,我们将编写一个应用,在无限循环中,要求用户输入一个浮点数。读取输入后,它以不同的格式类型显示该数字。要执行本练习,请完成以下步骤: 1. 包括用于流支持的`< iostream >`报头和用于`std::setprecision`支持的`< iomanip >` 2. 接下来,声明一个模板`格式打印`函数,该函数有一个名为`浮点`的模板参数,并接受一个这种类型的参数变量。接下来,通过调用`std::cout`对象中的`precision()`函数,将先前的精度存储在自动变量中。然后,在终端中以不同的格式显示给定的数字:带点、不带点,以及固定、科学、十六进制和默认浮点格式。接下来,在 for 循环中,从 0 到 22,以精度和循环计数器的大小显示给定的数字。循环退出后,使用我们之前存储的值设置回精度: ```cpp template< typename FloatingPoint > void formattingPrint(const FloatingPoint number) {      auto precision = std::cout.precision();      std::cout << "Default formatting with point: "                << std::showpoint << number << std::endl                << "Default formatting without point: "                << std::noshowpoint << number << std::endl                << "Fixed formatting: "                << std::fixed << number << std::endl                << "Scientific formatting: "                << std::scientific << number << std::endl                << "Hexfloat formatting: "                << std::hexfloat << number << std::endl                << "Defaultfloat formatting: "                << std::defaultfloat << number << std::endl;      for (int i = 0; i < 22; i++)      {           std::cout << "Precision: " << i                     << ", number: " << std::setprecision(i)                     << number << std::endl;      }      std::cout << std::setprecision(precision); } ``` 3. 进入`主`功能。声明一个名为`floatNum`的`float`变量、一个名为`doubleNum`的双变量和一个名为`longDoubleNum`的长双变量。然后在无限 while 循环中,要求用户输入一个浮点数,读取输入到`longDoubleNum`,传递到`formattingPrint`功能。接下来,使用`longDoubleNum`值初始化`doubleNum`,并将其传递给`formating print`功能。接下来,使用`longDoubleNum`值初始化`浮动`,并将其传递到`格式打印`功能: ```cpp int main(int argc, char **argv) {      float floatNum;      double doubleNum;      long double longDoubleNum;      while(true)      {           std::cout << "Enter the floating-point digit: ";           std::cin >> std::setprecision(36) >> longDoubleNum;           std::cout << "long double output" << std::endl;           formattingPrint(longDoubleNum);           doubleNum = longDoubleNum;           std::cout << "double output" << std::endl;           formattingPrint(doubleNum);           floatNum = longDoubleNum;           std::cout << "float output" << std::endl;           formattingPrint(floatNum);      }      return 0; } ``` 4. 构建并运行应用。跟随输出,输入有效位数为`22`的浮点值,如`0.2222222222222222222222222222222222`。我们会得到一个长输出。现在,我们需要把它分开来分析。下面是长双精度值输出的一部分截图: ![Figure 6.12: The result of executing Exercise 5, part 1](img/C14583_06_12.jpg) ###### 图 6.12:执行练习 5 第 1 部分的结果 我们可以看到,默认情况下,固定和`defaultfloat`编队只输出六个有效数字。通过科学的格式化,值的输出看起来与预期的一样。当我们调用`设定精度(0)`或`设定精度(1)`时,我们期望在该点之后不会输出任何数字。但是如果数字小于 1 设定精度,这将在点后留下一个数字。通过这样做,我们将看到直到 21 精度的正确输出。这意味着在我们的系统中,长双精度的最大精度是 20 个有效数字。现在,让我们分析双精度值的输出: ![Figure 6.13: The result of executing Exercise 5, part 2](img/C14583_06_13.jpg) ###### 图 6.13:执行练习 5 第 2 部分的结果 在这里,我们可以看到相同的格式化结果,但精度不同。不准确的输出从精度 17 开始。这意味着,在我们的系统中,双精度的最大精度是 16 个有效数字。现在,让我们分析浮点值的输出: ![Figure 6.14: The result of executing Exercise 5, part 3](img/C14583_06_14.jpg) ###### 图 6.14:执行练习 5 第 3 部分的结果 在这里,我们可以看到相同的格式化结果,但是精度不同。不准确的输出从精度 8 开始。这意味着,在我们的系统中,浮点数的最大精度是 8 位有效数字。不同系统上的结果应该是不同的。对它们的分析将帮助您为应用选择正确的数据类型。 #### 注意 切勿使用浮动数据类型来表示货币或汇率;你可能会得到错误的结果。 ### 用于布尔格式的输入/输出操纵器 在`< ios >`头文件中,有用于更改布尔格式的声明函数:`std::boolalpha`和`STD::nopoolalpha`。它们在没有参数的情况下被调用,并允许我们分别以文本或数字的方式显示布尔值。它们用于输入和输出操作。让我们考虑一个使用这些输入/输出操纵器进行输出操作的例子。我们将把布尔值显示为文本和数字: ```cpp std::cout << "Default formatting of bool variables: "           << "true: " << true           << ", false: " << false << std::endl; std::cout << "Formatting of bool variables with boolalpha flag is set: "           << std::boolalpha           << "true: " << true           << ", false: " << false << std::endl; std::cout << "Formatting of bool variables with noboolalpha flag is set: "           << std::noboolalpha           << "true: " << true           << ", false: " << false << std::endl; ``` 编译并运行此示例后,您将获得以下输出: ```cpp Default formatting of bool variables: true: 1, false: 0 Formatting of bool variables with boolalpha flag is set: true: true, false: false Formatting of bool variables with noboolalpha flag is set: true: 1, false: 0 ``` 如您所见,布尔变量的默认格式是使用`std::noboolalpha`标志执行的。为了在输入操作中使用这些函数,我们需要一个包含真/假单词或 0/1 符号的源字符串。输入操作中的`std::boolalpha`和`STD::nopoolalpha`函数调用如下: ```cpp bool trueValue, falseValue; std::istringstream iss("false true"); iss >> std::boolalpha >> falseValue >> trueValue; std::istringstream iss("0 1"); iss >> std::noboolalpha >> falseValue >> trueValue; ``` 如果您随后输出这些变量,您将看到它们通过读取布尔值被正确初始化。 ### 用于字段宽度和填充控制的输入/输出操纵器 在标准库中,也有通过输出字段的宽度进行操作的功能,当宽度大于输出数据时,应该使用哪些字符,这些填充字符应该插入到哪个位置。当您想要将输出对齐到左侧或右侧位置,或者想要用其他符号替换空格时,这些函数将非常有用。例如,假设您需要在两列中打印价格。如果使用标准格式,您将获得以下输出: ```cpp 2.33 3.45 2.2 4.55 3.67 3.02 ``` 这个看起来不太好,很难读懂。如果我们应用格式,输出将如下所示: ```cpp 2.33   3.45 2.2     4.55 3.67   3.02 ``` 这看起来更好。同样,您可能需要检查哪些字符用于填充空格,哪些字符实际上是您在数字之间插入的空格。例如,让我们将填充字符设置为“*”。您将获得以下输出: ```cpp 2.33* 3.45* 2.2** 4.55* 3.67* 3.02* ``` 现在,你可以看到空白处布满了星星。现在,我们已经考虑了格式化宽度和填充输出的有用之处,让我们考虑如何使用输入/输出操纵器来实现这一点。`std::setw`和`std::setfill`功能在`< iomanip >`头中声明。`std::setw`取整数值作为参数,将流的宽度设置为精确的 n 个字符。在少数情况下,宽度将设置为 0。它们如下: * 当用`std::string`或`char`调用换挡操作符时 * 当调用`std::put_money()`函数时 * 调用`std::quoted()`函数时 在`< ios >`表头中,有用于更改填充字符插入位置的声明功能:`std::internal`、`std::left`、`std::right`。它们仅用于输出操作,并且只影响整数、浮点和货币值。 现在,让我们考虑一个一起使用它们的例子。让我们输出宽度为 10 的正、负、浮点和十六进制值,并用“`#`”替换填充字符: ```cpp std::cout << "Internal fill: " << std::endl           << std::setfill('#')           << std::internal           << std::setw(10) << -2.38 << std::endl           << std::setw(10) << 2.38 << std::endl           << std::setw(10) << std::hex << std::showbase << 0x4b << std::endl; std::cout << "Left fill: " << std::endl           << std::left           << std::setw(10) << -2.38 << std::endl           << std::setw(10) << 2.38 << std::endl           << std::setw(10) << std::hex << std::showbase << 0x4b << std::endl; std::cout << "Right fill: " << std::endl           << std::right           << std::setw(10) << -2.38 << std::endl           << std::setw(10) << 2.38 << std::endl           << std::setw(10) << std::hex << std::showbase << 0x4b << std::endl; ``` 构建并运行此示例后,您将获得以下输出: ```cpp Internal fill: -#####2.38 ######2.38 0x######4b Left fill: -2.38##### 2.38###### 0x4b###### Right fill: #####-2.38 ######2.38 ######0x4b ``` ### 其他数字格式的输入/输出操纵器 如果需要输出一个带“+”号的正数值,可以从`< ios >`头中使用另一个 I/O 操纵器–`STD::show pos`功能。也存在与意义相反的操纵器——T4 标准:无显示功能。它们都对产出有影响。它们的使用非常容易。让我们考虑以下示例: ```cpp std::cout << "Default formatting: " << 13 << " " << 0 << std::endl; std::cout << "showpos flag is set: " << std::showpos << 13 << " " << 0 << std::endl; std::cout << "noshowpos flag is set: " << std::noshowpos << 13 << " " << 0 << std::endl; ``` 这里,我们使用默认格式进行输出,然后使用`std::showpos`标志,最后使用`std::noshowpos`标志。如果您构建并运行这个小示例,您会看到,默认情况下,`std::noshowpos`标志被设置。看看执行的结果: ```cpp Default formatting: 13 0 showpos flag is set: +13 +0 noshowpos flag is set: 13 0 ``` 您还希望输出浮点或十六进制数字的大写字符,以便您可以使用来自`< ios >`头:`std::大写`和`std::无符号`的函数。他们只处理输出。让我们考虑一个小例子: ```cpp std::cout << "12345.0 in uppercase with precision 4: "           << std::setprecision(4) << std::uppercase << 12345.0 << std::endl; std::cout << "12345.0 in no uppercase with precision 4: "           << std::setprecision(4) << std::nouppercase << 12345.0 << std::endl; std::cout << "0x2a in uppercase: "           << std::hex << std::showbase << std::uppercase << 0x2a << std::endl; std::cout << "0x2a in nouppercase: "           << std::hex << std::showbase << std::nouppercase << 0x2a << std::endl; ``` 在这里,我们输出带和不带`std::大写`标志的浮点和十六进制数字。默认情况下,设置`标准::无标签`标志。看看执行的结果: ```cpp 12345.0 in uppercase with precision 4: 1.234E+004 12345.0 in no uppercase with precision 4: 1.234e+004 0x2a in uppercase: 0X2A 0x2a in nouppercase: 0x2a ``` ### 用于空白处理的输入/输出操纵器 在标准库中,有处理空白的函数。来自`<的 **std::ws**`**函数是流>** 头,只对输入流起作用,并丢弃前导空格。来自`< ios >`头的`std::skipws`和`std::noskipws`功能用于控制前导空格的读写。它们为输入和输出流工作。当设置了`std::skipws`标志时,流会忽略字符序列输入前面的空白。默认情况下,设置`std::skipws`标志。让我们考虑一个使用这些输入/输出操纵器的例子。首先,我们将使用默认格式读取输入,并输出我们所读取的内容。接下来,我们将清除字符串并使用`std::noskipws`标志读取数据: ```cpp std::string name; std::string surname; std::istringstream("Peppy Ping") >> name >> surname; std::cout << "Your name: " << name << ", your surname: " << surname << std::endl; name.clear(); surname.clear(); std::istringstream("Peppy Ping") >> std::noskipws >> name >> surname; std::cout << "Your name: " << name << ", your surname: " << surname << std::endl; ``` 构建并运行此示例后,我们将获得以下输出: ```cpp Your name: Peppy, your surname: Ping Your name: Peppy, your surname: ``` 从前面的输出可以看出,如果我们设置`std::noskipws`标志,我们也会读取空白。 在`< iomanip >`表头中,已经声明了此表头的一个不寻常的操纵器:`std::报价`。当此函数应用于输入时,它用转义字符将给定的字符串用引号括起来。如果输入字符串已经包含转义引号,它也会读取它们。为了理解这一点,让我们考虑一个小例子。我们将使用不带引号的文本初始化一个源字符串,另一个字符串将使用带转义引号的文本初始化。接下来,我们将使用`std::ostringstream`读取它们,而不设置标志,并通过`std::cout`提供输出。看看下面的例子: ```cpp std::string str1("String without quotes"); std::string str2("String with quotes \"right here\""); std::ostringstream ss; ss << str1; std::cout << "[" << ss.str() << "]" << std::endl; ss.str(""); ss << str2; std::cout << "[" << ss.str() << "]" << std::endl; ``` 因此,我们将获得以下输出: ```cpp [String without quotes] [String with quotes "right here"] ``` 现在,让我们做同样的输出,但是使用`std::报价`调用: ```cpp std::string str1("String without quotes"); std::string str2("String with quotes \"right here\""); std::ostringstream ss; ss << std::quoted(str1); std::cout << "[" << ss.str() << "]" << std::endl; ss.str(""); ss << std::quoted(str2); std::cout << "[" << ss.str() << "]" << std::endl; ``` 现在,我们将有一个不同的结果: ```cpp ["String without quotes"] ["String with quotes \"right here\""] ``` 您是否注意到第一个字符串用引号括起来,第二个字符串的子字符串“就在这里”用转义字符存储? 现在,您知道如何在引号中包装任何字符串了。当使用`std::quoted()`时,甚至可以编写自己的包装来减少行数。例如,我们将流的工作转移到一个单独的函数: ```cpp std::string quote(const std::string& str) {      std::ostringstream oss;      oss << std::quoted(str);      return oss.str(); } ``` 然后,当我们需要调用包装器时,我们会执行以下操作: ```cpp std::string str1("String without quotes"); std::string str2("String with quotes \"right here\""); std::coot << "[" << quote(str1) << "]" << std::endl; std::cout << "[" << quote(str2) << "]" << std::endl; ``` 现在,它看起来好多了。第一个话题已经结束了,让我们复习一下刚刚学过的内容。在实践中,我们了解了预定义流对象的使用、带有内部内存的文件的输入/输出操作、输入/输出格式以及用户定义类型的输入/输出。现在我们已经完全理解了如何在 C++ 中使用 I/O 库,我们将考虑当标准流不够用时该怎么办。 ### 制作附加流 当提供的流接口不足以解决您的任务时,您可能希望创建一个额外的流来重用现有的接口之一。您可能需要从特定的外部设备输出或提供输入,或者您可能需要添加调用输入/输出操作的线程的标识。有几种方法可以做到这一点。您可以创建一个新的类,将现有流中的一个聚合为私有成员。它将通过已经存在的流函数实现所有需要的函数,例如移位运算符。另一种方法是继承一个现有的类,并以你需要的方式覆盖所有的虚函数。 首先,你必须选择合适的类来使用。您的选择应该取决于您想要添加的修改。如果需要修改输入或输出操作,选择`std::basic_istream`、`std::basic_ostream`、`std::basic_iostream`。如果要修改状态信息、控制信息、私有存储等,选择`std::ios_base`。如果您想修改与流缓冲区相关的内容,请选择`std::basic_ios`。选择正确的基类后,继承前面提到的类之一来创建一个额外的流。 还有一件事你必须知道——如何正确初始化标准流。在文件或字符串流和基本流类的初始化方面,有一些很大的区别。我们来复习一下。要初始化从文件流类派生的类的对象,需要传递文件名。若要初始化从字符串流类派生的类的对象,需要调用默认构造函数。它们都有自己的流缓冲区,因此在初始化时不需要额外的操作。要初始化从基本流类派生的类的对象,需要传递一个指向流缓冲区的指针。您可以创建一个缓冲区变量,也可以使用预定义流对象的缓冲区,如`std::cout`或`std::cerr`。 让我们详细回顾一下这两种创建附加流的方法。 ### 如何制作附加流–合成 组合意味着您将类的私有部分中的一些标准流对象声明为类成员。当您选择一个合适的标准流类时,转到它的头并注意它有哪个构造函数。然后,您需要在类的构造函数中正确初始化这个成员。要将您的类用作流对象,您需要实现基本函数,如 shift 运算符、`str()`等。您可能还记得,每个流类都为内置类型重载了移位运算符。他们还为预定义的功能(如`标准::endl`)提供了过载的换档操作符。您需要能够将您的类用作真正的流对象。我们只需要创建一个模板,而不是声明所有 18 个重载的移位运算符。此外,为了允许使用预定义的操纵器,我们必须声明一个带函数指针的移位运算符。 这看起来并不困难,所以让我们尝试为`std::ostream`对象实现这样一个“包装器”。 ### 练习 6:在用户定义的类中组成标准流对象 在本练习中,我们将创建自己的流对象,该对象包装了`std::ostream`对象并添加了附加特征。我们将创建一个名为`扩展流`的类,该类将向终端输出数据,并在每条输出的前面插入以下数据:日期和时间以及线程标识。要完成本练习,请执行以下步骤: 1. Include the required headers: `` for `std::endl` support, `` for `std::ostream` support, `` for `std::this_thread::get_id()` support, `` for `std::chrono::system_clock::now()`, and `` for converting timestamps into readable representations: #### 注意 不要忘了在 Eclipse 项目设置中添加 **pthread** 链接器标志以获得线程支持:**项目** - > **属性**->**C/c++ Build**->**设置** - > **G++ 链接器** - > **杂项** - > **链接器标志**输入'**-PTT 此路径对 Eclipse 版本 3.8.1 有效;不同版本可能会有所不同。** ```cpp #include #include #include #include #include ``` 2. 接下来,声明`扩展流`类。声明名为`m_oss`的`std::ostream`变量和名为`的 bool 变量 writeAdditionalInfo`。该 bool 变量将用于指示是否应打印扩展数据: ```cpp class extendedOstream { private:      std::ostream& m_oss;      bool writeAdditionalInfo; }; ``` 3. 接下来,在公共部分,定义一个默认构造函数并用`std::cout`初始化`m_oss`,以将输出重定向到终端。用`真`初始化`写附加信息`: ```cpp extendedOstream()      : m_oss(std::cout)      , writeAdditionalInfo(true) { } ``` 4. 定义一个模板重载的左移位运算符,`< <`,返回对`extendedstream`的引用,取一个模板参数值。然后如果`writeAdditionalInfo`为`true`,输出时间、线程 ID、给定值,然后将`writeAdditionalInfo`设置为`false`。如果`写附加信息`为`假`,只输出给定值。该功能将用于所有内置类型的输出: ```cpp template extendedOstream& operator<<(const T& value) {      if (writeAdditionalInfo)      {           std::string time = fTime();           auto id = threadId();           m_oss << time << id << value;           writeAdditionalInfo = false;      }      else      {           m_oss << value;      }      return *this; } ``` 5. 定义另一个重载左移位运算符,该运算符将指向函数的指针作为参数,并返回对`std::ostream`的引用。在函数体中,将`writeaddionaliinfo`设置为`true`,调用给定的函数,并将`m_oss`作为参数传递。该重载操作符将用于预定义的功能,如`std::endl` : ```cpp extendedOstream& operator<<(std::ostream& (*pfn)(std::ostream&)) {      writeAdditionalInfo = true;      pfn(m_oss);      return *this; } ``` 6. 在私有部分,定义`fTime`函数,该函数返回 std::string。它有一个系统时间。将其格式化为可读的表示形式并返回: ```cpp std::string fTime() {      auto now = std::chrono::system_clock::now();      std::time_t time = std::chrono::system_clock::to_time_t(now);      std::ostringstream oss;      std::string strTime(std::ctime(&time));      strTime.pop_back();      oss << "[" << strTime << "]";      return oss.str(); } ``` 7. 在私有部分,定义`threadId()`函数,该函数返回一个字符串。获取当前线程的`id`,格式化后返回: ```cpp std::string threadId() {      auto id = std::this_thread::get_id();      std::ostringstream oss;      oss << "[" << std::dec << id << "]";      return oss.str(); } ``` 8. 进入`主`功能。为了测试我们的流对象如何工作,创建一个名为`oss`的`extended stream`类型的对象。输出不同的数据,例如整数、浮点、十六进制和 bool: ```cpp extendedOstream oss; oss << "Integer: " << 156 << std::endl; oss << "Float: " << 156.12 << std::endl; oss << "Hexadecimal: " << std::hex << std::showbase     << std::uppercase << 0x2a << std::endl; oss << "Bool: " << std::boolalpha << false << std::endl; ``` 9. 然后,创建一个线程,用 lambda 函数初始化它,并将相同的输出放入 lambda 中。别忘了加入线程: ```cpp std::thread thr1([]()      {           extendedOstream oss;           oss << "Integer: " << 156 << std::endl;           oss << "Float: " << 156.12 << std::endl;           oss << "Hexadecimal: " << std::hex << std::showbase               << std::uppercase << 0x2a << std::endl;           oss << "Bool: " << std::boolalpha << false << std::endl;      }); thr1.join(); ``` 10. 现在,构建并运行应用。您将获得以下输出: ![](img/C14583_06_15.jpg) ###### 图 6.15:执行练习 6 的结果 考虑输出的每一行。可以看到输出的下一种格式:“[日期和时间][线程 ID]输出数据”。确保线程标识因线程而异。然后,数据以预期的格式输出。因此,如您所见,使用标准流的组合来实现您自己的输入/输出流对象并不太难。 ### 如何制作附加流–继承 继承意味着您创建自己的流类,并从具有虚拟析构函数的标准流对象中继承它。您的类必须是模板类,并且有模板参数,就像在父类中一样。要将所有继承的函数用于类的对象,继承应该是公共的。在构造函数中,您应该初始化父类,这取决于类的类型——用文件名、流缓冲区,或者默认情况下。接下来,您应该覆盖那些根据您的需求而改变的基本功能。 我们需要继承标准流类的最常见情况是,当我们想要为新设备(如套接字或打印机)实现输入/输出操作时。所有定义的标准流类都负责格式化输入和输出,并具有字符串、文件和终端的重载。只有`std::basic_streambuf`类负责处理设备,所以我们需要继承这个类,编写自己的实现,并将其设置为标准类的流缓冲区。`streambuf`类的核心功能是传输字符。它可以在刷新之间使用缓冲区存储字符,也可以在每次调用后立即刷新。这些概念被称为缓冲和非缓冲字符传输。 输出操作的缓冲字符传输工作如下: 1. 字符通过`sputc()`函数调用缓冲到内部缓冲区。 2. 当缓冲区已满时,`sputc()`调用受保护的虚拟成员,即`溢出()`。 3. `溢出()`功能将所有缓冲区内容传输到外部设备。 4. 当调用`pubsync()`函数时,它调用被保护的虚拟成员`sync()`。 5. `sync()`功能将所有缓冲区内容传输到外部设备。 用于输出操作的无缓冲字符传输的工作方式略有不同: 1. 字符被传递到`sputc()`功能。 2. `sputc()`函数立即调用被保护的虚拟成员`overflow()`。 3. `溢出()`功能将所有缓冲区内容传输到外部设备。 因此,对于输出操作的缓冲和非缓冲字符传输,我们应该覆盖`溢出()`和 sync()函数,它们执行实际工作。 输入操作的缓冲字符传输工作如下: 1. `sgetc()`函数从内部缓冲区读取字符。 2. `sgetc()`功能调用`sunetc()`功能,使消耗的角色再次可用。 3. 如果内部缓冲区为空,则`sgetc()`函数调用`下溢()`函数。 4. `下溢()`功能将字符从外部设备读取到内部缓冲区。 `sgetc()`和`下溢()`函数总是返回相同的字符。为了每次读取不同的字符,我们还有另外一对功能:`sbumpc()`和`uflow()`。用它们读字符的算法是一样的: 1. `sbumpc()`函数从内部缓冲区读取字符。 2. `sbumpc()`函数调用`sputback()`函数,使下一个字符可用于输入。 3. 如果内部缓冲区为空,则`sbumpc()`函数调用`uflow()`函数。 4. `uflow()`功能将字符从外部设备读取到内部缓冲区。 用于输入操作的无缓冲字符传输的工作原理如下: 1. `sgetc()`函数调用被称为`下溢()`的受保护虚拟成员。 2. `下溢()`功能将字符从外部设备读取到内部缓冲区。 3. `sbumpc()`函数调用名为`uflow()`的受保护虚拟成员。 4. `uflow()`功能将字符从外部设备读取到内部缓冲区。 如果出现任何错误,将调用名为`pbackfail()`的受保护虚拟成员来处理错误情况。如您所见,要覆盖`std::basic_streambuf`类,我们需要覆盖使用外部设备的虚拟成员。对于输入`streambuf`,我们应该覆盖`下溢()`、`uflow()`和`pbackfail()`成员。对于输出`streambuf`,我们应该覆盖`overflow()`和`sync()`成员。 让我们更详细地考虑所有这些步骤。 ### 练习 7:继承标准流对象 在本练习中,我们将创建一个名为`extended_streambuf`的类,该类继承自`std::basic_streambuf`。我们将使用`std::cout`流对象的一个缓冲区,并覆盖 overflow()函数,以便我们可以将数据写入外部设备(`stdout`)。接下来,我们将编写一个继承自`std::basic_ostream`类的`extended_ostream`类,并将一个流缓冲区设置为`extended_streambuf`。最后,我们将对我们的包装类做一些小的修改,并使用`extended_ostream`作为私有流成员。要完成本练习,请执行以下步骤: 1. 包括所需的标题: **< iostream >** 表示 **std::endl** 支持、**T22】s stream>T5 表示 **std::ostream** 和 **std::basic_streambuf** 支持、**T24】螺纹>T11 表示**STD::this _ thread::get _ id()**支持、**T26】chrono>****** 2. 创建一个名为`extended_streambuf`的模板类,它继承自`std::basic_streambuf`类。覆盖名为`overflow()`的公共成员,该成员向输出流中写入一个字符,并返回 EOF 或写入的字符: ```cpp template< class CharT, class Traits = std::char_traits > class extended_streambuf : public std::basic_streambuf< CharT, Traits > { public:     int overflow( int c = EOF ) override     {         if (!Traits::eq_int_type(c, EOF))         {             return fputc(c, stdout);         }         return Traits::not_eof(c);     } }; ``` 3. 接下来,创建一个名为`extended_ostream`的模板类,它是从`std::basic_ostream`类派生而来的。在私有部分,定义`extended_streambuf`类的一个成员,即 buffer。用缓冲成员初始化`std::basic_ostream`父类。接下来,在构造函数体中,从父类调用`init()`函数,以 buffer 作为参数。另外,重载`rdbuf()`函数,该函数返回一个指向缓冲变量的指针: ```cpp template< class CharT, class Traits = std::char_traits > class extended_ostream : public std::basic_ostream< CharT, Traits > { public:     extended_ostream()         : std::basic_ostream< CharT, Traits >::basic_ostream(&buffer)         , buffer()     {         this->init(&buffer);     }     extended_streambuf< CharT, Traits >* rdbuf () const     {         return (extended_streambuf< CharT, Traits >*)&buffer;     } private:     extended_streambuf< CharT, Traits > buffer; }; ``` 4. 将`扩展流`类重命名为记录器,以避免类似名称的误解。保持现有界面不变,但是用我们自己的流替换`std::ostream &`成员,即`对象- extended_ostream`。完整的类如下所示: ```cpp class logger { public:      logger()           : m_log()           , writeAdditionalInfo(true)      {      }      template      logger& operator<<(const T& value)      {           if (writeAdditionalInfo)           {                std::string time = fTime();                auto id = threadId();                m_log << time << id << value;                writeAdditionalInfo = false;           }           else           {                m_log << value;           }           return *this;      }      logger&      operator<<(std::ostream& (*pfn)(std::ostream&))      {           writeAdditionalInfo = true;           pfn(m_log);           return *this;      } private:      std::string fTime()      {           auto now = std::chrono::system_clock::now();           std::time_t time = std::chrono::system_clock::to_time_t(now);           std::ostringstream log;           std::string strTime(std::ctime(&time));           strTime.pop_back();           log << "[" << strTime << "]";           return log.str();      }      std::string threadId()      {           auto id = std::this_thread::get_id();           std::ostringstream log;           log << "[" << std::dec << id << "]";           return log.str();      } private:      extended_ostream m_log;      bool writeAdditionalInfo; }; ``` 5. 进入`主`功能,将`扩展数据流`对象更改为`记录器`对象。保持代码的其余部分不变。现在,构建并运行该练习。您将看到上一个练习中给出的输出,但是在本例中,我们使用了自己的流缓冲区、自己的流对象和向输出添加附加信息的包装类。查看下面截图中显示的执行结果,并将其与之前的结果进行比较。确保它们相似。如果是的话,这意味着我们做得很好,我们继承的类也像预期的那样工作: ![Figure 6.16: The result of executing Exercise 7](img/C14583_06_16.jpg) ###### 图 6.16:执行练习 7 的结果 在这个主题中,我们已经做了很多,并学习了如何以不同的方式创建额外的流。我们考虑了所有适合继承的类,以及哪个类更适合不同的需求。我们还学习了如何从基本 streambuf 类继承来实现与外部设备的工作。现在,我们将学习如何以异步方式使用输入/输出流。 ### 利用异步输入/输出 在很多情况下,输入/输出操作会花费大量时间,例如,创建备份文件、搜索大型数据库、读取大型文件等。您可以使用线程来执行输入/输出操作,而不会阻止应用的执行。但是对于某些应用来说,这不是处理长时间输入/输出的合适方式,例如,当每秒钟有数千个输入/输出操作时。在这种情况下,C++ 开发人员使用异步输入/输出。它节省了线程资源,并确保执行的线程不会被阻塞。让我们考虑一下什么是同步和异步输入/输出。 你可能还记得第 5 章,哲学家的晚餐——线程和并发,同步操作意味着一些线程调用操作并等待它完成。它可以是单线程或多线程应用。要点是线程正在等待输入/输出操作完成。 当操作不阻塞工作线程的执行时,异步执行发生。执行异步输入/输出操作的线程发送一个异步请求,并继续执行另一个任务。当操作完成时,初始线程将被通知完成,并且它可以根据需要处理结果。 由此看来,异步 I/O 比同步好很多,但这要看情况。如果您需要执行大量快速输入/输出操作,由于处理内核输入/输出请求和信号的开销,遵循同步方式会更合适。因此,在为应用开发架构时,您需要考虑所有可能的场景。 标准库不支持异步输入/输出操作。因此,为了利用异步输入/输出,我们需要考虑替代库或编写自己的实现。首先,让我们考虑平台相关的实现。然后,我们将看看跨平台库。 ### Windows 平台上的异步 I/O Windows 支持各种设备的 I/O 操作:文件、目录、驱动器、端口、管道、套接字、终端等等。一般来说,我们对所有这些设备使用相同的输入/输出接口,但某些设置因设备而异。让我们考虑在 Windows 中对文件进行输入/输出操作。 因此,在 Windows 中,我们需要打开一个设备,并为其获取一个处理程序。不同的设备以不同的方式打开。要打开文件、目录、驱动器或端口,我们使用 **<窗口头中的**创建文件**功能。要打开管道,我们使用**创建命名管道**功能。要打开一个套接字,我们使用 socket()和 accept()函数。打开一个终端,我们使用**CreateConsoleScreenBuffer**和 **GetStdHandle** 功能。它们都返回一个设备处理程序,该程序用于处理该设备的所有功能。** `CreateFile`函数采用七个参数来管理打开设备的工作。函数声明如下所示: ```cpp HANDLE CreateFile( PCTSTR pszName,                    DWORD  dwDesiredAccess,                    DWORD  dwShareMode,                    PSECURITY_ATTRIBUTES psa,                    DWORD  dwCreationDisposition,                    DWORD  dwFlagsAndAttributes,                    HANDLE hFileTemplate); ``` 第一个参数是`pszName`–文件的路径。第二个参数调用`dwdesireaccess`并管理对设备的访问。它可以采用以下值之一: ```cpp 0 // only for configuration changing GENERIC_READ // only reading GENERIC_WRITE // only for writing GENERIC_READ | GENERIC_WRITE // both for reading and writing ``` 第三个参数`dwShareMode`管理当文件已经打开时,操作系统应该如何处理所有新的`创建文件`调用。它可以采用以下值之一: ```cpp 0 // only one application can open device simultaneously FILE_SHARE_READ // allows reading by multiple applications simultaneously FILE_SHARE_WRITE // allows writing by multiple applications simultaneously FILE_SHARE_READ | FILE_SHARE_WRITE // allows both reading and writing by multiple applications simultaneously FILE_SHARE_DELETE // allows moving or deleting by multiple applications simultaneously ``` 第四个参数`psa`通常设置为`空`。第五个参数`dwCreationDisposition`管理文件是打开还是创建。它可以采用以下值之一: ```cpp CREATE_NEW // creates new file or fails if it is existing CREATE_ALWAYS // creates new file or overrides existing OPEN_EXISTING // opens file or fails if it is not exists OPEN_ALWAYS // opens or creates file TRUNCATE_EXISTING // opens existing file and truncates it or fails if it is not exists ``` 第六个参数`dwFlagsAndAttributes`,管理缓存或使用文件。它可以采用以下值之一来管理缓存: ```cpp FILE_FLAG_NO_BUFFERING // do not use cache FILE_FLAG_SEQUENTIAL_SCAN // tells the OS that you will read the file sequentially FILE_FLAG_RANDOM_ACCESS // tells the OS that you will not read the file in sequentially FILE_FLAG_WR1TE_THROUGH // write without cache but read with ``` 它可以采用以下值之一来管理文件工作: ```cpp FILE_FLAG_DELETE_ON_CLOSE // delete file after closing (for temporary files) FILE_FLAG_BACKUP_SEMANTICS // used for backup and recovery programs FILE_FLAG_POSIX_SEMANTICS // used to set case sensitive when creating or opening a file FILE_FLAG_OPEN_REPARSE_POINT // allows to open, read, write, and close files differently FILE_FLAG_OPEN_NO_RECALL // prevents the system from recovering the contents of the file from archive media FILE_FLAG_OVERLAPPED // allows to work with the device asynchronously ``` 它可以采用下列文件属性值之一: ```cpp FILE_ATTRIBUTE_ARCHIVE // file should be deleted FILE_ATTRIBUTE_ENCRYPTED // file is encrypted FILE_ATTRIBUTE_HIDDEN // file is hidden FILE_ATTRIBUTE_NORMAL // other attributes are not set FILE_ATTRIBUTE_NOT_CONTENT_ INDEXED // file is being processed by the indexing service FILE_ATTRIBUTE_OFFLINE // file is transferred to archive media FILE_ATTRIBUTE_READONLY // only read access FILE_ATTRIBUTE_SYSTEM // system file FILE_ATTRIBUTE_TEMPORARY // temporary file ``` 最后一个参数`hFileTemplate`将打开文件的处理程序或`空值`作为参数。如果文件处理程序通过,则`创建文件`功能将忽略所有属性和标志,并使用打开文件的属性和标志。 以上就是关于`创建文件`参数。如果无法打开设备,则返回`无效 _ 句柄 _ 值`。以下示例演示如何打开文件进行读取: ```cpp #include #include int main() {      HANDLE hFile = CreateFile(TEXT("Test.txt"), GENERIC_READ,                                 FILE_SHARE_READ | FILE_SHARE_WRITE,                                 NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);      if (INVALID_HANDLE_VALUE == hFile)          std::cout << "Failed to open file for reading" << std::endl;      else          std::cout << "Successfully opened file for reading" << std::endl;      CloseHandle(hFile);      return 0; } ``` 接下来,为了执行输入操作,我们使用`读取文件`功能。它以文件描述符为第一个参数,源缓冲区为第二个参数,最大读取字节数为第三个参数,读取字节数为第四个参数,同步执行的`空`值或指向有效且唯一的重叠结构的指针为最后一个参数。如果操作成功,`读取文件`返回真,否则返回假。下面的示例演示如何从以前打开的文件中输入内容以供读取: ```cpp BYTE pb[20]; DWORD dwNumBytes; ReadFile(hFile, pb, 20, &dwNumBytes, NULL); ``` 为了执行输出操作,我们使用`写文件`功能。它的声明与`ReadFile`相同,但是第三个参数设置了要写入的字节数,第五个参数是写入的字节数。下面的示例演示如何输出到以前打开的文件进行写入: ```cpp BYTE pb[20] = "Some information\0"; DWORD dwNumBytes; WriteFile(hFile, pb, 20, &dwNumBytes, NULL); ``` 要将缓存数据写入设备,请使用`FlushFileBuffer`功能。它只需要一个参数——文件描述符。让我们转到异步输入/输出。要让操作系统知道您计划与设备异步工作,您需要使用`文件 _ 标志 _ 重叠`标志打开它。现在,打开文件进行写入或读取,如下所示: ```cpp #include #include int main() {      HANDLE hFile = CreateFile(TEXT("Test.txt"), GENERIC_READ,                                 FILE_SHARE_READ | FILE_SHARE_WRITE,                                 NULL, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, NULL);      if (INVALID_HANDLE_VALUE == hFile)          std::cout << "Failed to open file for reading" << std::endl;      else          std::cout << "Successfully opened file for reading" << std::endl;      CloseHandle(hFile);      return 0; } ``` 我们使用相同的操作来执行文件的读取或写入,即`读取文件`和`写入文件`,唯一的区别是读取或写入的字节数被设置为空,我们必须传递一个有效且唯一的`重叠`对象。让我们考虑一下重叠对象的结构: ```cpp typedef struct _OVERLAPPED { DWORD  Internal; // for error code DWORD  InternalHigh; // for number of read bytes DWORD  Offset; DWORD  OffsetHigh; HANDLE hEvent; // handle to an event } OVERLAPPED, *LPOVERLAPPED; ``` 内部成员设置为`STATUS_PENDING`,表示操作还没有开始。读取或写入的字节数将被写入`内部高`成员。`偏移`和`偏移`在异步操作中被忽略。`hEvent`成员用于接收关于异步操作完成的事件。 #### 注意 输入/输出操作的顺序没有保证,因此您不能依赖于此。如果你计划在一个地方写一个文件,在另一个地方读一个文件,你不能依赖这个顺序。 在异步模式下使用`读文件`和`写文件`有一点不同寻常。如果输入/输出请求是同步执行的,它们将返回非零值。如果他们返回`假`,您需要调用`GetLastError`功能来检查为什么返回`假`。如果错误代码为`ERROR_IO_PENDING`,这意味着输入/输出请求已成功处理,处于挂起状态,稍后将执行。 你要记住的最后一点是,在输入输出操作完成之前,你不能移动或移除带有数据的`重叠的`对象或缓冲区。对于每个输入/输出操作,您应该创建一个新的重叠对象。 最后,让我们考虑系统通知我们完成输入/输出操作的方式。有一些这样的机制:释放设备、释放事件、产生警报和使用输入/输出端口。 **“坏”方式**:`写文件`和`读文件`功能将设备设置为“占用”状态。当输入/输出操作完成时,驱动程序将设备设置为“空闲”状态。我们可以检查完成的输入/输出操作是调用`等待单对象`还是`等待多对象`功能。以下示例演示了这种方法: ```cpp #include #include int main() {      HANDLE hFile = CreateFile(TEXT("Test.txt"), GENERIC_READ,                                      FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,                                      OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, NULL);      BYTE bBuffer[100];      OVERLAPPED o = { 0 };      BOOL bResult = ReadFile(hFile, bBuffer, 100, NULL, &o);      DWORD dwError = GetLastError();      if (bResult && (dwError == ERROR_IO_PENDING))      {           WaitForSingleObject(hFile, INFINITE);           bResult = TRUE;      }      CloseHandle(hFile);      return 0; } ``` 这是检查输入/输出操作是否已完成的最简单方法。但是这种方法使调用线程等待`WaitForSingleObject`调用,所以它变成了同步调用。此外,您可以为此设备启动一些输入/输出操作,但您不能确定线程会在设备需要的版本上唤醒。 **好一点,但不是最好的做法**:你还记得重叠结构的最后一个成员吗?通过调用`创建事件`功能创建一个事件,并将其设置为`重叠`对象。然后,当输入输出操作完成时,系统通过调用`设置事件`功能来释放该事件。接下来,当调用线程需要获得一个正在执行的 I/O 操作的结果时,您调用`WaitForSingleObject`并传递这个事件的描述符。以下示例演示了这种方法: ```cpp #include #include int main() {      HANDLE hFile = CreateFile(TEXT("Test.txt"), GENERIC_READ,                                FILE_SHARE_READ | FILE_SHARE_WRITE,                                NULL, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, NULL);      BYTE bInBuffer[10];      OVERLAPPED o = { 0 };      o.hEvent = CreateEvent(NULL,TRUE,FALSE,"IOEvent");      ReadFile(hFile, bInBuffer, 10, NULL, &o);      ///// do some work      HANDLE hEvent = o.hEvent;      WaitForSingleObject(hEvent, INFINITE);      CloseHandle(hFile);      return 0; } ``` 如果您希望通知调用线程输入/输出操作的结束,这是一个非常简单的方法。但是这并不是实现这一点的理想方式,因为当有很多这样的操作时,您需要为每个操作创建一个事件对象。 **还有一种不是最好的方法**:可报警的输入/输出以下列方式工作。我们将`ReadFileEx`和`WriteFileEx`称为输入/输出。它们类似于标准的`ReadFile`和`WriteFile`,但是我们不传递存储读或写字符数的变量,我们传递回调函数的地址。这个回调函数被称为完成例程,并具有以下声明: ```cpp VOID WINAPI CompletionRoutine(DWORD dwError,                   DWORD dwNumBytes,                   OVERLAPPED* po); ``` `ReadFileEx``WriteFileEx`将回调函数的地址传递给设备驱动。当设备上的操作完成时,驱动程序将回调函数的地址添加到 APC 队列中,并将指针添加到重叠结构中。然后,操作系统调用这个函数,并传递读或写字节数、错误代码和指向重叠结构的指针。 这种方法的主要缺点是编写回调函数和使用大量全局变量,因为回调函数在上下文中只有少量信息。不使用这种方法的另一个原因是,只有调用线程可以接收关于完成的通知。 现在我们已经讨论了糟糕的情况,让我们看看处理输入/输出结果的最佳方法——输入/输出端口。输入/输出完成端口被开发用于线程池。为了创建这样一个端口,我们使用`CreateIoCompletionPort`。这个函数的声明如下: ```cpp HANDLE CreateIoCompletionPort(HANDLE hFile,                        HANDLE hExistingCompletionPort,                        ULONG_PTR CompletionKey,                        DWORD dwNumberOfConcurrentThreads); ``` 该函数创建一个输入/输出完成端口,并将设备与该端口相关联。要完成这个动作,我们需要调用两次。为了创建新的完成端口,我们调用`CreateIoCompletionPort`函数并传递`INVALID_HANDLE_VALUE`作为第一个参数,NULL 作为第二个参数,0 作为第三个参数,并传递这个端口的线程数。将 0 作为第四个参数传递会将线程数设置为等于处理器数。 #### 注意 对于输入/输出完成端口,建议使用等于处理器数量两倍的线程数量。 接下来,我们需要将这个端口与输入/输出设备相关联。因此,我们第二次调用`CreateIoCompletionPort`函数,传递一个设备的描述符,一个所创建的完成端口的描述符,一个指示读取或写入设备的常量,以及作为线程数的 0。然后,当我们需要得到完成的结果时,我们从我们的端口描述符中调用`GetQueuedCompletionStatus`。如果操作完成,函数会立即返回一个结果。如果没有,那么线程等待完成。以下示例演示了这种方法: ```cpp #include #include int main() {     HANDLE hFile = CreateFile(TEXT("Test.txt"), GENERIC_READ,                               FILE_SHARE_READ | FILE_SHARE_WRITE,                               NULL, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, NULL);     HANDLE m_hIOcp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);     CreateIoCompletionPort(hFile, m_hIOcp, 1, 0);     BYTE bInBuffer[10];     OVERLAPPED o = { 0 };     ReadFile(hFile, bInBuffer, 10, NULL, &o);     DWORD dwNumBytes;     ULONG_PTR completionKey;     GetQueuedCompletionStatus(m_hIOcp, &dwNumBytes, &completionKey, (OVERLAPPED**) &o, INFINITE);     CloseHandle(hFile);     return 0; } ``` ### Linux 平台上的异步 I/O Linux 上的异步输入/输出支持对不同设备的输入和输出,如套接字、管道和 TTYs,文件除外。是的,这很奇怪,但是 Linux 开发人员认为对文件的输入/输出操作足够快。 要打开输入/输出设备,我们使用 open()函数。它有以下声明: ```cpp int open (const char *filename, int flags[, mode_t mode]) ``` 第一个参数是文件名,而第二个参数是控制文件打开方式的位掩码。如果系统无法打开设备,open()将返回-1 值。如果成功,它会返回一个设备描述符。打开模式的可能标志有`O_RDONLY`、`O_WRONLY`和`O_RDWR`。 为了执行输入/输出操作,我们使用名为`aio`的`POSIX`接口。它们有一组定义好的功能,例如`aio_read`、`aio_write`、`aio_fsync`等等。它们用于启动异步操作。为了得到执行的结果,我们可以使用信号通知或者线程的实例化。或者,我们可以选择完全不被通知。全部在`< aio.h >`表头声明。 这些几乎都是以`aiocb`结构(异步 IO 控制块)为参数。它控制输入输出操作。该结构的声明如下: ```cpp struct aiocb {     int aio_fildes;     off_t aio_offset;     volatile void *aio_buf;     size_t aio_nbytes;     int aio_reqprio;     struct sigevent aio_sigevent;     int aio_lio_opcode; }; ``` `aio_fildes`成员是打开的设备的描述符,而`aio_offset`成员是设备中应该执行读或写操作的偏移量。`aio_buf`成员是一个指向要读取或写入的缓冲区的指针。`aio_nbytes`成员是缓冲区的大小。`aio_reqprio`成员是该 io 操作执行的优先级。`aio_sigevent`成员是一个指出应该如何通知调用线程完成的结构。`aio_lio_opcode`成员是一种输入输出操作。下面的例子演示了如何初始化`aiocb`结构: ```cpp std::string fileContent; constexpr int BUF_SIZE = 20; fileContent.resize(BUF_SIZE, 0); aiocb aiocbObj; aiocbObj.aio_fildes = open("test.txt", O_RDONLY); if (aiocbObj.aio_fildes == -1) {      std::cerr << "Failed to open file" << std::endl;      return -1; } aiocbObj.aio_buf = const_cast(fileContent.c_str()); aiocbObj.aio_nbytes = BUF_SIZE; aiocbObj.aio_reqprio = 0; aiocbObj.aio_offset = 0; aiocbObj.aio_sigevent.sigev_notify = SIGEV_SIGNAL; aiocbObj.aio_sigevent.sigev_signo = SIGUSR1; aiocbObj.aio_sigevent.sigev_value.sival_ptr = &aiocbObj; ``` 这里,我们创建了一个读取文件内容的缓冲区,即`文件内容`。然后,我们创建了一个名为`aiocbObj`的`aiocb`结构。接下来,我们打开一个文件进行读取,并检查该操作是否成功。然后,我们将指针设置为一个缓冲区和一个缓冲区大小。缓冲区大小告诉驱动程序应该读取或写入多少字节。接下来,我们指出,我们将通过将偏移量设置为 0 来从文件的开头读取。然后,我们在`SIGEV_SIGNAL`中设置通知类型,这意味着我们希望获得关于完成操作的信号通知。然后,我们设置信号编号,这将触发关于完成的通知。在我们的例子中,是`sigusr 1`–用户定义的信号。接下来,我们将指向`aiocb`结构的指针设置为信号处理器。 在创建并正确初始化`aiocb`结构后,我们可以执行输入或输出操作。让我们完成一个练习,了解如何在 Linux 平台上使用异步输入/输出。 ### 练习 8:在 Linux 中异步读取文件 在本练习中,我们将开发一个以异步方式从文件中读取数据并将读取的数据输出到控制台的应用。当执行读取操作时,驱动器使用触发信号通知应用。要进行本练习,请执行以下步骤: 1. 包括所有必需的头: **< aio.h >** 支持异步读写, **< signal.h >** 支持信号, **< fcntl.h >** 支持文件操作,**T21】unist . h>**支持符号常量, **< iostream >** 输出到终端, **< chrono >** 2. 创建一个名为**的布尔变量,它将指示操作何时完成: ```cpp bool isDone{}; ```** 3. 定义将成为我们的信号处理器的函数,即`aioSigHandler`。异步操作完成后将调用它。信号处理器应具有以下签名: ```cpp void name(int number, siginfo_t* si, void* additional) ``` 4. 第一个参数是信号编号,第二个参数是包含信号生成原因信息的结构,最后一个参数是附加信息。它可以被转换成`ucontext_t`结构的指针,这样我们就可以接收到被这个信号中断的线程上下文。在`aioSigHandler`中,使用`SI_ASYNCIO`检查关于异步输入/输出操作的信号是否恒定。如果是,输出一条消息。接下来,将`isDone`设置为`true` : ```cpp void aioSigHandler(int no, siginfo_t* si, void*) {      std::cout << "Signo: " << no << std::endl;      if (si->si_code == SI_ASYNCIO)      {           std::cout << "I/O completion signal received" << std::endl;      }      isDone = true; } ``` 5. 定义另一个名为`的帮助功能,启动`。它将初始化`信号`结构。这个结构定义了在输入/输出操作结束时将发送哪个信号,以及应该调用哪个处理程序。这里,我们选择了`sigusr 1`–一个用户自定义的信号。在`sa_flags`中,设置我们希望在动作重启或收到信息时发送该信号: ```cpp bool initSigAct(struct sigaction& item) {      item.sa_flags = SA_RESTART | SA_SIGINFO;      item.sa_sigaction = aioSigHandler;      if (-1 == sigaction(SIGUSR1, &item, NULL))      {           std::cerr << "sigaction usr1 failed" << std::endl;           return false;      }      std::cout << "Successfully set up a async IO handler to SIGUSR1 action" << std::endl;      return true; } ``` 6. 定义名为`fillAiocb`的帮助函数,用给定的参数填充`aiocb`结构。它将引用 aiocb 结构、文件描述符、缓冲区指针和缓冲区大小作为参数。在`SIGUSR1`中设置`sigev_signo`,我们之前已经初始化过: ```cpp void fillAiocb(aiocb& item, const int& fileDescriptor,           char* buffer, const int& bufSize) {      item.aio_fildes = fileDescriptor;      item.aio_buf = static_cast(buffer);      item.aio_nbytes = bufSize;      item.aio_reqprio = 0;      item.aio_offset = 0;      item.aio_sigevent.sigev_notify = SIGEV_SIGNAL;      item.aio_sigevent.sigev_signo = SIGUSR1;      item.aio_sigevent.sigev_value.sival_ptr = &item; } ``` 7. 进入`主`功能。定义名为`buf_size`的变量,它保存缓冲区大小。创建一个这样大小的缓冲区: ```cpp constexpr int bufSize = 100; char* buffer = new char(bufSize); if (!buffer) {      std::cerr << "Failed to allocate buffer" << std::endl;      return -1; } ``` 8. 创建一个名为`文件名`的变量,该变量保存一个名为“`Test.txt`的文件。然后,以只读方式打开该文件: ```cpp const std::string fileName("Test.txt"); int descriptor = open(fileName.c_str(), O_RDONLY); if (-1 == descriptor) {      std::cerr << "Failed to opene file for reading" << std::endl;      return -1; } std::cout << "Successfully opened file for reading" << std::endl; ``` 9. 创建一个`信号`结构,并使用`初始化信号`功能进行初始化: ```cpp struct sigaction sa; if (!initSigAct(sa)) {      std::cerr << "failed registering signal" << std::endl;      return -1; } ``` 10. 创建一个`aiocb`结构,并使用`fillaocb`函数进行初始化: ```cpp aiocb aiocbObj; fillAiocb(aiocbObj, descriptor, buffer, bufSize); ``` 11. 使用`aio_read`功能执行`读取`操作: ```cpp if (-1 == aio_read(&aiocbObj)) {      std::cerr << "aio_read failed" << std::endl; } ``` 12. 接下来,在循环中,评估`isDone`变量。如果是假的,让线程休眠`3 毫秒`。通过这样做,我们将等待输入/输出操作完成: ```cpp while (!isDone) {      using namespace std::chrono_literals;      std::this_thread::sleep_for(3ms); } std::cout << "Successfully finished read operation. Buffer: " << std::endl << buffer; ``` 13. Before running this exercise, create a `Test.txt` file in the project directory and write different symbols. For example, our file contains the following data: ```cpp a1a"1 a1\a1 a1 a1 a1a"1 a1\a1 a1 a1 a1a"1 a1\a1 a1 a1 a1a"1 a1\a1 a1 a1 a1a"1 a1\a1 a1 a1 a1a"1 a1\a1 a1 a1 a1a"1 a1\a1 a1 a1 a1a"1 a1\a1 a1 a1 a1a"1 a1\a1 a1 a1 a1a"1 a1\a1 a1 a1 a1a"1 a1\a1 a1 a1 a1a"1 a1\a1 a1 a1 a1a"1 a1\a1 a1 a1 a1a"1 a1\a1 a1 a1 ``` 这里有字母字符、数字字符、特殊符号、空格、制表字符和换行符。 14. 现在,在您的集成开发环境中构建并运行这个练习。您的输出将类似于以下内容: ![](img/C14583_06_17.jpg) ###### 图 6.17:执行练习 8 的结果 您可以看到该文件已成功打开进行读取,并且我们已成功为其设置了`SIGUSR1`信号和处理程序。然后,我们收到了 30 号信号,也就是`SI_ASYNCIO`信号。最后,我们可以输出我们所阅读的内容,并将其与文件内容进行比较。通过这样做,我们可以确保所有数据都被正确读取。 这就是 Linux 系统中异步输入/输出的全部内容。 #### 注意 你可以通过 Linux 的手册页找到更多关于 Linux 中异步 IO 的信息:[http://man7.org/linux/man-pages/man7/aio.7.html](7.html)。 现在,让我们了解一下我们可以为跨平台应用使用什么。 ### 异步跨平台输入输出库 我们已经考虑了异步 I/O 的特定于平台的决策。现在,要编写一个跨平台的应用,您可以使用这些特定于平台的方法,并将其与预处理器指令一起使用;例如: ```cpp #ifdef WIN #include #else #include #endif ``` 在这两个头中,您可以为特定于平台的实现声明相同的接口。您还可以实现自己的 AIO 库,它将在单独的线程中使用一些状态机或队列。此外,您可以使用一些实现必要功能的免费库。最受欢迎的库是`Boost。Asio`。它为异步工作提供了许多接口,例如: * 无线程并发 * 线 * 缓冲 * 流 * 协同程序 * TCP、UDP 和 ICMP * 套接字 * 加密套接字协议层 * 倍 * 串行端口 让我们简单考虑一下它的输入输出操作接口。我们可以使用`Asio`库的接口进行同步和异步操作。所有的输入输出操作都从`io_service`类开始,该类提供核心的输入输出功能。在``头文件中声明。同步输入/输出调用`io_service`对象的`run()`函数进行单个操作,该操作阻塞调用线程,直到作业完成。异步输入输出使用`run()`、`run_one()`、`poll()`和`poll_one()`功能。`run()`函数运行事件循环来处理请求处理程序。`run_one()`函数也是如此,但是事件循环应该只处理一个处理程序。`poll()`函数运行事件循环来执行所有就绪的处理程序。`poll_one()`做同样的事情,但只针对一个处理程序。以下示例演示了所有这些函数的用法: ```cpp boost::asio::io_service io_service1; io_service1.run(); boost::asio::io_service io_service2; io_service2.run_one(); boost::asio::io_service io_service3; io_service3.poll(); boost::asio::io_service io_service4; io_service4.poll_one(); ``` 有可能在调用实际的输入/输出操作之前运行事件处理程序。使用带有`io_service`类的工作类在代码中实现这个特性。工作类保证运行函数不会返回,直到您决定以后不再有任何输入/输出操作。例如,您可以使工作类成为另一个类的成员,并将其从析构函数中移除。因此,在您的课程期间,`io_service`将运行: ```cpp boost::asio::io_service io_service1; boost::asio::io_service::work work(io_service1); io_service1.run(); boost::asio::io_service io_service2; boost::asio::io_service::work work(io_service2); io_service2.poll(); ``` 接下来,要执行任何输入/输出操作,我们需要输入/输出设备的确切位置,例如,文件、套接字等。实现工作的类有很多,使用不同的 I/O 设备,例如``头中的`boost::asio::IP::TCP::socket`。接下来,为了读写套接字,我们使用`boost::asio::async_read`和`boost::asio::async_write`。它们以一个套接字、`boost::asio::buffer`、回调函数为参数。当异步操作被执行时,回调函数被调用。我们可以将 lambda 函数作为回调函数传递,或者使用 boost::bind 函数绑定现有函数。`boost::bind`创建一个可调用对象。以下示例演示了如何使用`Boost::Asio`写入套接字: ```cpp boost::asio::io_service ioService; tcp::socket socket; int length = 15; char* msg = new char(length); msg = "Hello, world!"; auto postHandler = [=]() {      auto writeHandler = [=](boost::system::error_code ec, std::size_t length)      {           if (ec)           {                socket_.close();           }           else           {                // wrote length characters           }      };      boost::asio::async_write(socket, boost::asio::buffer(msg, length), writeHandler); }; ioService.post(postHandler); ``` 这里,我们使用 lambda 函数作为异步 I/O 操作的回调。 #### 注意 `升压。Asio`在[https://www . boost . org/doc/libs/1 _ 63 _ 0/doc/html/boost _ asio . html](7.html)上有详细记录。有很多不同输入输出设备和不同方法的例子。如果您决定使用`Boost,可以参考本文档。Asio`在你的项目中。 在这里,我们考虑了实现异步输入/输出操作的不同方法。根据您的需求、环境和允许的实用程序,您可以选择适当的方式在应用中实现异步输入/输出。请记住,如果您选择执行许多快速输入/输出操作,最好以同步方式进行,因为它不会占用大量系统资源。既然我们知道了如何利用异步输入/输出,那么让我们学习如何在多线程应用中使用输入/输出。 ### 线程和输入输出的交互 输入/输出标准库不是线程安全的。在标准库的文档中,我们可以找到一种解释,说明对流或流缓冲区的并发访问会导致数据竞争,从而导致未定义的行为。为了避免这种情况,我们应该使用我们在*第 5 章*、*哲学家的晚餐-线程和并发*中学习的技术来同步对流和缓冲区的访问。 让我们稍微谈谈`std::cin`和`std::cout`对象。对它们的每个调用都是线程安全的,但是让我们考虑以下示例: ```cpp std::cout << "Counter: " << counter << std::endl; ``` 在这一行中,我们看到`std::cout`被调用了一次,但是对轮班操作员的每次调用实际上是对`std::cout`对象的不同调用。因此,我们可以将这一行改写如下: ```cpp std::cout << "Counter: "; std::cout << counter; std::cout << std::endl; ``` 这段代码和前面的单行代码做得完全一样,也就是说,如果你从不同的线程调用这个单行代码,你的输出将是混合的,不清晰的。您可以对其进行修改,使其真正成为线程安全的,如下所示: ```cpp std::stringsream ss; ss << "Counter: " << counter << std::endl; std::cout << ss.str(); ``` 因此,如果您使用第二种方法输出到终端,您的输出将是清晰的和线程安全的。这种行为可能会有所不同,具体取决于编译器或标准库版本。你也要知道`std::cout`和`std::cin`在其中是同步的。这意味着调用`std::cout`总是刷新`std::cin`流,调用`std::cin`总是刷新`std::cout`流。 最好的方法是将所有输入/输出操作包装在一个保护类中,该类将使用互斥体控制对流的访问。如果您需要使用`std::cout`从多个线程输出到终端,您可以实现一个非常简单的类,它除了锁定互斥体和调用`std::cout`之外什么也不做。让我们完成一个练习并创建这样的类。 ### 练习 9:为 std::cout 开发线程安全包装器 在本练习中,我们将开发一个简单的`std::cout`包装器,它产生线程安全的输出。我们将编写一个小测试函数来检查它是如何工作的。让我们开始并执行以下步骤: 1. Include all the required headers: ```cpp #include // for std::cout #include    // for std::thread #include     // for std::mutex #include   // for std::ostringstream ``` 现在,让我们想想我们的包装。我们可以在某个地方创建这个类的变量,并将其传递给每个创建的线程。然而,这是一个糟糕的决定,因为在复杂的应用中,这将需要大量的努力。我们也可以作为一个单独的个体这样做,这样我们就可以从任何地方访问它。接下来,我们要思考我们课程的内容。实际上,我们可以使用我们在*练习 7* 、*继承标准流对象*中创建的类。在那个练习中,我们重载了`std::basic_streambuf`和`std::basic_ostream`,并将`std::cout`设置为输出设备。我们可以给重载函数添加一个互斥体,并按原样使用它。请注意,我们不需要任何额外的逻辑——只需要使用`std::cout`的输出数据。为此,我们可以创建一个更简单的类。如果我们没有设置输出设备,应用左移位操作符将不会生效,并将待输出的数据存储在内部缓冲区中。太好了。现在,我们需要考虑如何使用`std::cout`将这个缓冲区输出。 2. 实现诸如`write()`这样的函数,该函数将锁定一个互斥体,并从内部缓冲区输出到`std::cout`。该功能的用法如下: ```cpp mtcout cout; cout << msg << std::endl; cout.write(); ``` 3. 我们有一个函数总是会被自动调用,我们可以把 write 函数的代码放进去。这是一个析构器。在这种情况下,我们把创造和毁灭结合成一条线。这样一个对象的用法如下: ```cpp mtcout{} << msg << std::endl; ``` 4. 现在,让我们定义我们的`mtcout`(多线程 cout)类。它有一个公共默认构造函数。在私有部分,它有一个静态互斥变量。您可能还记得,静态变量在类的所有实例之间共享。在析构函数中,我们使用 cout 锁定互斥体和输出。在输出中添加一个前缀——当前线程的 ID 和一个空格字符: ```cpp class mtcout : public std::ostringstream { public:      mtcout() = default;      ~mtcout()      {      std::lock_guard lock(m_mux);           std::cout << std::this_thread::get_id() << " " << this->str();      } private:      static std::mutex m_mux; }; ``` 5. 接下来,在类外声明`互斥`变量。我们这样做是因为我们必须在任何源文件中声明一个静态变量: ```cpp std::mutex mtcout::m_mux; ``` 6. 进入主功能。创建一个名为`的函数`。它将测试我们的`mtcout`班。它以字符串为参数,使用`mtcout`在从`0`到`1000`的循环中输出该字符串。使用`std::cout`添加相同的输出并注释掉。比较两种情况下的输出: ```cpp auto func = [](const std::string msg) {      using namespace std::chrono_literals;      for (int i = 0; i < 1000; ++ i)      {           mtcout{} << msg << std::endl; //          std::cout << std::this_thread::get_id() << " " << msg << std::endl;      } }; ``` 7. 创建四个线程,并传递一个 lambda 函数作为参数。向每个线程传递不同的字符串。最后,连接所有四个线程: ```cpp std::thread thr1(func, "111111111"); std::thread thr2(func, "222222222"); std::thread thr3(func, "333333333"); std::thread thr4(func, "444444444"); thr1.join(); thr2.join(); thr3.join(); thr4.join(); ``` 8. Build and run the exercise for the first time. You will get the following output: ![Figure 6.18: The result of executing Exercise 9, part 1](img/C14583_06_18.jpg) ###### 图 6.18:执行练习 9 第 1 部分的结果 在这里,我们可以看到每个线程都输出自己的消息。该消息没有被中断,输出看起来很清晰。 9. 现在,用λ中的`std::cout`取消输出注释,并用`mtcout`注释输出。 10. Again, build and run the application. Now, you will get a "dirty", mixed output, like the following: ![Figure 6.19: The result of executing Exercise 9, part 2](img/C14583_06_19.jpg) ###### 图 6.19:执行练习 9 第 2 部分的结果 您可以看到这种混合输出,因为我们不输出单个字符串;相反,我们调用`std::cout`四次: ```cpp std::cout << std::this_thread::get_id(); std::cout << " "; std::cout << msg; std::cout << std::endl; ``` 当然,我们可以在输出字符串之前对其进行格式化,但是使用 mtcout 类更方便,并且不必担心格式化问题。您可以为任何流创建类似的包装器,以便安全地执行输入/输出操作。您可以更改输出并添加任何附加信息,例如当前线程的 ID、时间或您需要的任何信息。利用我们在*第 5 章*、*哲学家的晚餐——线程和并发*中了解到的东西,同步输入/输出操作,扩展流,并使输出对您的需求更加有用。 ### 使用宏 在本章的活动中,我们将使用宏定义来简化和美化我们的代码,所以让我们复习一下如何使用它们。宏定义是预处理器指令。宏定义的语法如下: ```cpp #define [name] [expression] ``` 这里,[name]是任何有意义的名称,[expression]是任何小函数或值。 当预处理器面对宏名时,它用表达式替换它。例如,假设您有以下宏: ```cpp #define MAX_NUMBER 15 ``` 然后,在代码中的一些地方使用它: ```cpp if (val < MAX_NUMBER) while (val < MAX_NUMBER) ``` 预处理器完成工作后,代码如下: ```cpp if (val < 15) while (val < 15) ``` 预处理器对函数做同样的工作。例如,假设您有一个用于获取最大数量的宏: ```cpp #define max(a, b) a < b ? b : a ``` 然后,在代码中的一些地方使用它: ```cpp int res = max (5, 3); std::cout << (max (a, b)); ``` 预处理器完成工作后,代码如下: ```cpp int res = 5 < 3 ? 3 : 5; std::cout << (a < b ? b : a); ``` 作为表达式,您可以使用任何有效的表达式,如函数调用、内联函数、值等。如果需要在多行中编写表达式,请使用反斜杠运算符“\”。例如,我们可以用两行写的最大定义如下: ```cpp #define max(a, b) \ a < b ? b : a ``` #### 注意 宏定义来自 C 语言。最好使用常量变量或内联函数。然而,仍然有使用宏定义更方便的情况,例如,在记录器中,当您希望定义不同的日志记录级别时。 现在。我们知道完成活动所需的一切。所以,让我们总结一下这一章所学的内容,让我们改进一下我们在*第 5 章**哲学家的晚餐——线程和并发*中所写的项目。我们将开发一个线程安全的记录器,并将其集成到我们的项目中。 ### 活动 1:美术馆模拟器的记录系统 在本练习中,我们将开发一个记录器,将格式化的日志输出到终端。我们将以以下格式输出日志: ```cpp [dateTtime][threadId][logLevel][file:line][function] | message ``` 我们将为不同的日志记录级别实现宏定义,而不是直接调用。这个记录器将是线程安全的,我们将从不同的线程同时调用它。最后,我们将把它整合到项目中——美术馆模拟器。我们将运行模拟并观察漂亮打印的日志。我们将创建一个额外的流,使用并发流,并格式化输出。我们将实现本章中所学的几乎所有内容。我们还将采用上一章中的同步技术。 因此,在尝试本练习之前,请确保您已经完成了本章前面的所有练习。 在实现这个应用之前,让我们描述一下我们的类。我们有以下新创建的类: ![Figure 6.20: Descriptions of the classes that should be implemented](img/C14583_06_20.jpg) ###### 图 6.20:应该实现的类的描述 我们还在艺术画廊模拟器项目中实现了以下类: ![](img/C14583_06_21.jpg) ###### 图 6.21:美术馆模拟器项目中已经实现的类的表 在开始实现之前,让我们将新的类添加到类图中。所有描述的具有关系的类都由下图组成: ![Figure 6.22: The class diagram](img/C14583_06_22.jpg) ###### 图 6.22:类图 为了接收所需格式的输出,`记录器`类应该具有以下`静态`功能: ![Figure 6.23: Descriptions of the LoggerUtils member functions](img/C14583_06_23.jpg) ###### 图 6.23:记录器成员函数的描述 按照以下步骤完成本活动: 1. 定义并实现`记录器`类,它提供了一个输出格式化的接口。它包含将给定数据格式化为所需表示形式的静态变量。 2. 定义并实现`StreamLogger`类,它为输出到终端提供了一个线程安全的接口。它应该像这样格式化输出: ```cpp [dateTtime][threadId][logLevel][file:line: ][function] | message ``` 3. 在一个单独的头文件中,声明不同日志记录级别的宏定义,这些宏定义返回`StreamLogger`类的一个临时对象。 4. 将实现的记录器集成到艺术画廊模拟器的类中。 5. 用适当的宏定义调用替换`std::cout`的所有调用。 在实现了上述步骤之后,您应该在终端上获得一些关于所有实现的类的日志的输出。看一看,确保日志以所需的格式输出。预期产出应如下: ![Figure 6.24: The result of the application's execution](img/C14583_06_24.jpg) ###### 图 6.24:应用执行的结果 #### 注意 这项活动的解决方案可以在第 696 页找到。 ## 总结 在这一章中,我们学习了 C++ 中的输入输出操作。我们考虑了输入输出标准库,它为同步输入输出操作提供了一个接口。此外,我们考虑了异步输入/输出的平台相关本机工具和`增强。Asio`库,用于跨平台异步 I/O 操作。我们还学习了如何在多线程应用中使用输入/输出流。 我们从标准库为输入/输出操作提供的基本特性开始。我们了解了预定义的流对象,例如`std::cin`和`std::cout`。在实践中,我们学习了如何使用标准流和重写移位运算符来轻松读写自定义数据类型。 接下来,我们练习了如何创建附加流。我们继承了基本的流类,实现了自己的流缓冲类,并在练习中练习了它们的用法。我们了解了最适合继承的流类,并考虑了它们的优缺点。 然后,我们考虑了在不同操作系统上异步输入/输出操作的方法。我们简要考虑了使用被称为`Boost 的跨平台 I/O 库。Asio`,提供同步和异步操作的接口。 最后,我们学习了如何在多线程应用中执行输入/输出操作。我们通过构建多线程记录器将所有这些新技能付诸实践。我们创建了一个记录器抽象,并将其用于美术馆模拟器。因此,我们创建了一个简单、清晰和健壮的日志记录系统,允许我们使用日志轻松调试应用。总之,我们利用了本章所学的一切。 在下一章中,我们将更仔细地研究测试和调试应用。我们将从学习断言和安全网开始。然后,我们将练习为接口编写单元测试和模拟。之后,我们将在 IDE 中练习调试应用:我们将使用断点、观察点和数据可视化。最后,我们将编写一个活动来掌握测试代码的技巧。 ================================================ FILE: docs/adv-cpp/08.md ================================================ # 八、每个人都会跌倒,这是你爬起来的方式——测试和调试 ## 学习目标 本章结束时,您将能够: * 描述不同类型的断言 * 实现编译时和运行时断言 * 实现异常处理 * 描述和实现单元测试和模拟测试 * 使用断点和观察点调试 C++ 代码 * 在调试器中检查数据变量和 C++ 对象 在本章中,您将学习如何适当地添加断言,添加单元测试用例以使代码按照需求运行,并学习调试技术以发现代码中的错误并跟踪其根本原因。 ## 简介 在**软件开发生命周期** ( **SDLC** )中,一旦需求收集阶段完成,那么通常会进入设计和架构阶段,其中项目的高层流程被定义并分解成更小的模块组件。当一个项目中有许多团队成员时,每个团队成员都必须被明确分配到模块的特定部分,并且他们知道自己的需求。通过这种方式,他们可以在一个隔离的环境中独立编写自己的那部分代码,并确保它可以正常工作。一旦他们的部分工作完成,他们可以将他们的模块与其他开发人员的模块集成,并确保整个项目按照要求执行。 这个概念也可以应用到小项目中,在小项目中,开发人员完全在处理一个需求,将它分解成更小的组件,在隔离的环境中开发组件,确保它按照计划执行,集成所有的小模块来完成项目,最后测试它以确保整个项目运行良好。 当整个项目被集成和执行时,需要大量的测试。可能会有一个单独的团队(称为**质量保证**,即 **QA** )专门执行这项任务。与其在项目级别发现问题,不如在每个独立的阶段测试代码。该测试需要由负责该模块的开发人员来执行。这种类型的测试被称为单元测试。在这里,开发人员可以模拟运行该模块所需的环境,并确保他们测试模块中编写的功能的特定部分。例如,让我们假设,在一个大项目中,有一个小模块,其功能是解析配置文件并获取设置环境所需的参数。如果解析文件的部分代码将一个`IP 地址`作为`字符串`,那么开发人员需要确保它的格式为`XXX。XXX.XXX.XXX`,其中`X`为`0` - `9`的数字。字符串的长度必须是有限的。 在这里,开发人员可以创建一个测试程序来执行他们的那部分代码:解析文件,将`IP 地址`提取为字符串,并测试它的格式是否正确。同样,如果配置有其他参数需要解析,并且它们需要采用特定的格式,例如`用户标识` / `密码`,日志或挂载点的文件位置等等,那么所有这些都将是该模块单元测试的一部分。在本章中,我们将解释诸如`断言`、`安全嵌套` ( `异常处理`)、`单元测试`、`嘲讽`、`断点`、`观察点`、`数据可视化`等技术,以查明错误来源并限制其增长。在下一节中,我们将探讨断言技术。 ### 断言 为上述场景使用测试条件将有助于项目以更好的方式开发,因为缺陷将在基础级别而不是在后期的质量保证级别被发现。可能存在这样的场景,即使在编写单元测试用例并成功执行代码之后,当应用崩溃、程序意外退出或行为不符合预期时,也可能会发现问题。为了克服这种情况,通常,开发人员使用调试模式二进制文件来重新创建问题。`断言`用于确保检查条件,否则程序执行终止。 这样,问题就可以快速追踪到。此外,在`调试模式`下,开发人员可以逐行遍历程序的实际执行情况,并检查代码流是否如预期的那样,或者变量是否按预期设置并正确访问。有时,如果指针变量没有指向有效的内存位置,访问指针变量会导致意外的行为。 在编写代码时,我们可以检查必要条件是否满足。如果没有,那么程序员可能不想进一步执行代码。使用断言可以很容易地做到这一点。一个`断言`是一个宏,其中特定条件被检查,如果它不满足标准,中止被调用(程序执行被停止)并且一个错误信息被打印为一个`标准错误`。这通常是一个**运行时断言**。也可以在编译时进行断言。这个我们以后再说。在下一节中,我们将解决一个练习,其中我们将编写和测试我们的第一个断言。 ### 练习 1:编写和测试我们的第一个断言 在本练习中,我们将编写一个函数来解析一个`IP 地址`,并检查它是否有效。作为我们要求的一部分,`IP 地址`将作为字符串文字在`XXX 中传递。XXX.XXX.XXX`格式。在此格式中,`X`代表从`0` - `9`的一个数字。因此,作为检查正在解析的`字符串`是否正确的测试的一部分,我们需要确保`字符串`不为`空`,并且其`长度`小于`16`。按照以下步骤实施本练习: 1. 创建一个名为**的新文件。** 2. Open the file and write the following code to include the header files: ```cpp #include #include #include using std::cout; using std::endl; ``` 在前面的代码中,`#include < cassert >`表明我们需要在定义 assert 的地方包含`cassert`文件。 3. Create a function named `checkValidIp()` that will take the `IP address` as input and return a `true` value if the `IP address` meets our requirements. Write the following code to define the function: ```cpp bool checkValidIp(const char * ip){     assert(ip != NULL);     assert(strlen(ip) < 16);     cout << "strlen: " << strlen(ip) << endl;     return true; } ``` 这里,`断言(ip!=空)`表示如果传递的“`ip`变量不是`空值`,断言宏用于检查条件。如果是`空`,那么它将中止并显示一条错误消息。此外,`assert(strlen(IP)<16)`显示该 assert 用于检查“`ip`”是否为`16`字符或更少。如果不是,则中止并显示`错误信息`。 4. Now, create a `main` function that passes a different string literal to our `checkValidIp()` function and makes sure it can be tested appropriately. Write the following code for the `main` function: ```cpp int main(){     const char * ip;     ip = NULL;     bool check = checkValidIp(ip);     cout << " IP address is validated as :" << (check ? "true" : "false") << endl;     return 0; } ``` 在前面的代码中,我们特意将`NULL`传递给了`ip`变量,以确保`断言`被调用。 5. Open the `Command Prompt` and go to the location where the **AssertSample.cpp** file is stored. Compile it with the `g++ ` compiler by typing the following command: ```cpp g++ AssertSample.cpp ``` 利用该命令,生成`a.out`二进制文件。 6. Run the `a.out` binary file by typing the following command in the compiler: ```cpp ./a.out ``` 您将看到以下输出: ![Figure 7.1: Running the Assertion binary on the Command Prompt](img/C14583_07_01.jpg) ###### 图 7.1:在命令提示符下运行断言二进制文件 在前面的截图中,您可以看到三段用红色圈起来的代码。第一个突出显示的部分显示了**的编译。cpp** 文件。第二个突出显示的部分显示了由前面的编译生成的 **a.out** 二进制文件。第三个突出显示的部分显示了为传递的**空值**抛出错误的断言。它指示调用断言的行号和函数名。 7. Now, inside the `main` function, we will pass `ip` with a length greater than `16` and check if the `assert` is called here too. Write the following code to implement this: ```cpp ip = "111.111.111.11111"; ``` 再次,打开编译器,编译**。cpp** 文件,并运行生成的二进制文件。编译器中将显示以下输出: ![Figure 7.2: Running the Assertion binary on the Command Prompt](img/C14583_07_02.jpg) ###### 图 7.2:在命令提示符下运行断言二进制文件 在前面的截图中,断言抛出了一个错误,因为传递的`ip`长度大于`16`。 8. Now, to satisfy the `assert` condition so that the binary runs fine, we need to update the value of `ip` inside the `main` function. Write the following code to do this: ```cpp ip = "111.111.111.111"; ``` 再次,打开编译器,编译**。cpp** 文件,并运行生成的二进制文件。编译器中会显示以下输出: ![Figure 7.3: Running the Assertion binary on the Command Prompt](img/C14583_07_03.jpg) ###### 图 7.3:在命令提示符下运行断言二进制文件 #### 注意 因为我们只是在这里处理`断言`,所以我们没有给我们的`checkValidIP()`函数添加任何额外的功能。然而,我们将在*异常处理*和*单元测试*部分使用相同的例子,其中我们将为我们的函数添加更多的功能。 9. 如果我们不希望可执行文件由于生产或发布环境中的断言而中止,请从代码中删除`断言`宏调用。首先更新长度大于`16`的`ip`的值。将以下代码添加到文件中: ```cpp ip = "111.111.111.11111"; ``` 10. Now, during compilation, pass the `-DNDEBUG` macro. This will make sure that the assert is not called in the binary. Write the following command to compile our `.cpp` file in the Terminal: ```cpp g++ -DNDEBUG AssertSample.cpp ``` 之后,当我们执行二进制文件时,会生成以下输出: ![](img/C14583_07_04.jpg) ###### 图 7.4:在命令提示符下运行断言二进制文件 在上一个截图中,由于`断言`没有被调用,它将显示字符串长度为 **17** ,而**真**值为将被验证的 IP 地址。在本练习中,我们看到在执行二进制文件时调用了断言。在代码编译期间,我们也可以有一个断言。这是在 C++ 11 中引入的。它被称为**静态断言**,我们将在下一节中探讨它。 ### 静态断言 有时,我们可以在编译时进行条件检查,以避免将来出现任何错误。例如,在一个项目中,我们可能会使用一个第三方库,其中声明了一些数据结构。这个结构信息,比如它的`大小`和`成员变量`,我们从它的头文件中就知道了。利用这些信息,我们可以正确地分配或释放内存,以及处理其成员变量。在具有不同版本的第三方库中,此结构属性可能会改变。然而,如果我们的项目代码仍然使用早期版本的结构,那么当我们使用它时,它会产生问题。运行二进制文件时,我们可能会在稍后阶段遇到错误。我们可以在编译时使用`静态断言`来捕捉这个错误。我们可以比较静态数据,比如库的版本号,从而确保我们的代码不会遇到任何问题。在下一节中,我们将基于此解决一个练习。 ### 练习 2:测试静态断言 在本练习中,我们将通过执行`静态断言`来比较两个头文件的版本号。如果`版本号`小于`1`,则会抛出静态断言错误。执行以下步骤来实施本练习: 1. Create a header file named **PersonLibrary_ver1.h** and add the following code: ```cpp #ifndef __PERSON_H__ #define __PERSON_H__ #include using std::string; #define PERSON_LIB_VERSION 1 struct person{     string name;     int age;     string address; }; #endif ``` 在前面的代码中,结构人被定义并由以下属性组成:`姓名`、`年龄`和`地址`。它还有版本号`1`。 2. Create another header file named **PersonLibrary_ver2.h** and add the following code: ```cpp #ifndef __PERSON_H__ #define __PERSON_H__ #include using std::string; #define PERSON_LIB_VERSION 2 struct person{     string name;     int age;     string address;     string Mobile_No; }; #endif ``` 在前面的代码中,定义了`结构人`,由以下属性组成:`姓名`、`年龄`、`地址`、`手机号`。它还有`版本号 2`。现在,`版本 1`是老版本,`版本 2`是新版本。下面是并排的两个头文件的截图: ![Figure 7.5: Library file with a different version](img/C14583_07_05.jpg) ###### 图 7.5:不同版本的库文件 3. Create a file named **StaticAssertionSample.cpp** and add the following code: ```cpp #include #include"PersonLibrary.h" void doSanityCheck(){     static_assert(PERSON_LIB_VERSION > 1 , "PERSON LIBRARY VERSION not greater than 1");     // Do any more sanity check before starting app ... } int main(){     doSanityCheck();     return 0; } ``` 在前面的代码中,我们在构建和执行项目之前,对项目进行了健全性检查。我们已经创建了一个名为`Dosaniticheck()`的函数来执行库的版本检查。它使用静态断言完成,并在编译时执行。代码的第二行显示包含**personal library . h**文件。在`Dosaniticheck()`函数中,`static_assert()`函数检查该版本的库是否大于 1。 #### 注意 如果您的项目需要在库的`版本 2`或更高版本中定义的人员结构来正确执行它,我们需要匹配`版本 2`的文件,即`PERSON_LIB_VERSION`至少应该设置为`2`。如果开发人员获得了库的`版本 1`,并试图为项目创建一个二进制文件,这可能会在执行中产生问题。为了避免这种情况,在项目的主要代码中,我们在项目构建和执行之前对其进行了健全性检查。 4. To include `version 1` of the library in our **.cpp** file, open the terminal and write the following command: ```cpp ln -s PersonLibrary_ver1.h PersonLibrary.h ``` 前面的命令将创建一个名为**personal library . h**的**personal library _ ver 1 . h**文件的软链接。这就像用`版本 1`模拟我们使用**personal library . h**的环境。 5. Compile our **.cpp** file using the following command in the terminal: ```cpp g++ StaticAssertionSample.cpp ``` 以下是终端生成的输出: ![Figure 7.6: Seeing a static error](img/C14583_07_06.jpg) ###### 图 7.6:看到静态错误 在前面的截图中,三个区域用红色圈起来。第一个给出了创建软链接的命令。第二个命令显示了我们创建的**personal library . h**文件。第三个区域显示了由于库的版本不匹配而引发的`static_assert`错误。 6. Now, to compile the program correctly, remove the soft link of `ProgramLibrary` and create a new one pointing to `version2` and compile it again. This time, it will compile fine. Type the following commands into the terminal to remove a soft link: ```cpp rm PersonLibrary.h ln -s PersonLibrary_ver2.h PersonLibrary.h g++ StaticAssertionSample.cpp ``` 以下是相同内容的截图: ![Figure 7.7: Static assertion compilation file](img/C14583_07_07.jpg) ###### 图 7.7:静态断言编译文件 如您所见,用红色标记的区域显示使用了正确版本的`personal library`,编译顺利进行。编译后,创建一个名为“**a.exe**的二进制文件。在本练习中,我们通过比较两个头文件的版本号来执行静态断言。在下一节中,我们将探讨异常处理的概念。 ### 了解异常处理 正如我们之前在调试模式二进制中看到的,当某个条件不满足时,我们可以使用运行时断言来中止程序。但是在发布模式二进制或生产环境中,当客户端使用该产品时,突然中止程序并不是一个好主意。最好处理这种错误情况,并继续执行二进制文件的下一部分。 最糟糕的情况发生在二进制文件需要退出的时候。它将通过添加正确的日志消息并清除为该进程分配的所有内存来优雅地完成这项工作。对于这种情况,使用异常处理。这里,当遇到错误情况时,执行转移到一个特殊的代码块。例外情况包括以下三个部分: * **试块**:这里我们检查条件是否符合必要条件。 * **掷块**:如果条件不匹配,则抛出异常。 * **捕获块**:它捕获异常,并针对该错误条件执行必要的执行。 在下一节中,我们将解决一个练习,其中我们将对代码执行异常处理。 ### 练习 3:执行异常处理 在本练习中,我们将对我们的 **AssertSample.cpp** 代码执行异常处理。我们将用异常替换断言条件。执行以下步骤来实施本练习: 1. 创建一个名为`ExceptionSample.cpp`的文件。 2. 添加以下代码添加头文件: ```cpp #include #include using std::cout; using std::endl; ``` 3. Create a `checkValidIp()` function wherein we have a try-catch block. If the condition present in the try block is not satisfied, an exception will be thrown and the message in the catch block will be printed. Add the following code to accomplish this: ```cpp bool checkValidIp(const char * ip){     try{         if(ip == NULL)             throw ("ip is NULL");         if(strlen(ip) > 15)             throw int(strlen(ip));     }     catch(const char * str){         cout << "Error in checkValidIp :"<< str << endl;         return false;     }     catch(int len){         cout << "Error in checkValidIp, ip len:" << len <<" greater than 15 characters, condition fail" << endl;         return false;     }     cout << "strlen: " << strlen(ip) << endl;     return true; } ``` 在前面的代码中,您可以看到检查条件的 try 块。在 try 块中,如果`ip`为`空`,那么它将抛出(`const char *`)类型的异常。在下一种情况下,如果`ip`大于`15`,则抛出 int 参数类型的异常。该投掷由具有匹配参数(`int`或`const char *`)的正确接球完成。两个异常都返回`假`,并显示一些错误信息。或者,在`catch`块中,如果需要任何清理,您可以执行额外的步骤,或者使用异常中用于比较的变量的默认值。 #### 注意 存在默认异常;例如,如果有一个嵌套函数用不同的参数抛出了一个错误,那么它可以作为一个更高级别的函数用 catch(…)这样的参数来捕获。同样,在泛型 catch 中,您可以为异常处理创建默认行为。 4. 创建`main()`函数,并在其中编写以下代码: ```cpp int main(){     const char * ip;     ip = NULL;     if (checkValidIp(ip))         cout << "IP address is correctly validated" << endl;     else {         /// work on error condition         // if needed exit program gracefully.         return -1;     }     return 0; } ``` 5. Open the terminal, compile our file, and run the binary. You will see the following output: ![Figure 7.8: Example execute code with exception handling](img/C14583_07_08.jpg) ###### 图 7.8:带有异常处理的示例执行代码 前面的例子抛出了`ip`为`空`的异常,优雅的退出。 6. 现在,通过提供超过`15`个字符,修改`主`功能中`ip`的值。为此,编写以下代码: ```cpp ip = "111.111.111.11111"; ``` 7. Open the terminal, compile our file, and run the binary. You will see the following output: ![Figure 7.9: Another example of exception handling](img/C14583_07_09.jpg) ###### 图 7.9:异常处理的另一个例子 对于 **ip 字符串**,它抛出一个长度不匹配的**错误。** 8. 再次修改`主`功能中`ip`的值,少于`15`个字符。为此,编写以下代码: ```cpp ip = "111.111.111.111"; ``` 9. 打开终端,编译我们的文件,运行二进制。您将看到以下输出: ![Figure 7.10: The binary runs fine without throwing an exception](img/C14583_07_10.jpg) ###### 图 7.10:二进制运行良好,没有抛出异常 从前面的截图可以看出,二进制文件执行正常,没有任何异常。既然您已经理解了如何处理异常,在下一节中,我们将探讨`单元测试`和`模拟测试`的概念。 ## 单元测试和模拟测试 当开发人员开始编写代码时,他们需要确保代码在单元级别得到正确测试。可能会发生边界条件丢失的情况,代码在客户端运行时可能会中断。为了避免这种情况,一般来说,对代码进行`单元测试`是个好主意。`单元测试`是在代码的单元级或基础级执行的测试,开发人员可以在隔离的环境中测试他们的代码,假设运行代码的某个特性所需的设置已经完成。一般来说,将模块分解成小函数并分别测试每个函数是一种很好的做法。 例如,假设部分功能是读取配置文件,并使用配置文件中的参数设置环境。我们可以创建一个专门的函数来编写这个功能。因此,为了测试这个函数,我们可以创建一组单元测试用例,检查各种可能失败或行为不正确的组合。一旦确定了这些测试用例,开发人员就可以编写代码来覆盖功能,并确保它通过所有的单元测试用例。作为开发的一部分,这是一个很好的实践,您可以继续首先添加测试用例,并相应地添加代码,然后运行该函数的所有测试用例,并确保它们的行为适当。 有很多工具可以用来为项目编写和集成单元测试用例。几个是 **cppunit** 、 **Google Test** 、**微软单元测试框架**、 **catch** 。为了我们例子的目的,我们将致力于`谷歌测试框架`。它是免费提供的,可以与项目集成。它使用**xuit 测试框架**,并且有一个断言集合,可以用来测试测试用例的条件。在下一节中,我们将解决一个练习,其中我们将创建我们的第一个单元测试用例。 ### 练习 4:创建我们的第一个单元测试用例 在本练习中,我们将处理上一节中讨论的相同场景,其中开发人员负责编写一个函数来解析`配置文件`。配置文件中传递了不同的有效参数,如`产品可执行名称`、`版本号`、`数据库连接信息`、`IP 地址`连接服务器等等。假设开发人员将在一个单独的函数中分解解析文件、设置和测试单个属性的参数的所有功能。在我们的案例中,我们假设开发人员正在编写功能,他们已经将`IP 地址`解析为`字符串`,并想推断`字符串`是否是有效的`IP 地址`。目前,匹配`IP 地址`有效的标准需要满足以下条件: * `字符串`不应为空。 * `字符串`包含的字符不得超过`16` * `弦`应该在`XXX。XXX.XXX.XXX`格式,其中`X`必须是`0` - `9`之间的数字。 执行以下步骤来实施本练习: 1. Create the **CheckIp.h** header file and write the following code inside it: ```cpp #ifndef  _CHECK_IP_H_ #define _CHECK_IP_H_ include include using namespace std; bool checkValidIp(const char *); #endif ``` 在前面的代码中,我们编写了一个名为`checkValidIp()`的函数来检查`IP 地址`是否有效。同样,为了理解`谷歌单元测试`,我们将编写最少的代码来理解这个特性。 2. Create a **CheckIp.cpp** file and write the following code, wherein we'll check if `ip` is `not NULL` and that the length is less than `16`: ```cpp #include "CheckIp.h" #include #include bool checkValidIp(const char * ip){     if(ip == NULL){         cout << "Error : IP passes is NULL " << endl;         return false;     }     if(strlen(ip) > 15){         cout << "Error: IP size is greater than 15" << endl;         return false;     }     cout << "strlen: " << strlen(ip) << endl;     return true; } ``` 在前面的代码中,如果两个条件都失败,函数返回`false`。 3. 调用`checkValidIp()`函数创建一个名为 **MainIp.cpp** 的新文件。这个文件,一般来说,将包含项目的主要流程,但是为了我们练习的目的,我们只是调用我们的`checkValidIP()`函数。在里面添加以下代码: ```cpp #include"CheckIp.h" int main(){     const char * ip;     //ip = "111.111.111.111";     ip = "111.111.111.11111";     if (checkValidIp(ip))         cout << "IP address is correctly validated" << endl;     else {         /// work on error condition         // if needed exit program gracefully.         cout << " Got error in valid ip " << endl;         return -1;     }     return 0; } ``` 4. To create test code, we'll create our first **.cpp** file, that is, **TestCases.cpp**. This will contain test cases for our `checkValidIp` function. Write the following code inside it: ```cpp #include"CheckIp.h" #include using namespace std; const char * testIp; TEST(CheckIp, testNull){     testIp=NULL;     ASSERT_FALSE(checkValidIp(testIp)); } TEST(CheckIp, BadLength){     testIp = "232.13.1231.1321.123";     ASSERT_FALSE(checkValidIp(testIp)); } ``` 在前面代码的第二行,我们包含了 **gtest.h** 文件。我们还使用 **TEST** 函数调用测试用例,该函数接受两个参数:第一个是 **testsuite** 名称,第二个是 **testcase** 名称。对于我们的案例,我们已经创建了**测试套件** **检查点**。在**测试**块中,您将看到我们有**谷歌测试**定义了一个名为 **ASSERT_FALSE** 的**断言,它将检查条件是否为 **false** 。如果不是,它将通过测试用例,并在结果中显示相同的内容。** #### 注意 一般来说,对于一个`谷歌测试`用例和测试套件,你可以将它们分组在一个公共的名称空间中,并调用`RUN_ALL_TESTS`宏,该宏运行所有附加到测试二进制文件的测试用例。对于每个测试用例,它调用`SetUp`函数进行初始化(像类中的构造函数),然后调用实际的测试用例,最后调用`拆卸`函数(像类中的析构函数)。没有必要编写`SetUp`和`拆卸`功能,除非你必须为测试用例初始化一些东西。 5. Now, to run the test cases, we will create the main **.cpp** file for the test cases and call the `RUN_ALL_TESTS` macro. Alternatively, we can create an executable by linking the `Google Test library` that invokes `RUN_ALL_TESTS`. For our case, we will do the latter. Open the terminal and run the following command to create a test run binary: ```cpp g++ -c CheckIp.cpp ``` 这将包括 **CheckIp.cpp** 的对象文件,因为其中定义了`CheckValidIp`函数。 6. 现在,键入以下命令添加创建二进制文件所需的库: ```cpp g++ CheckIp.o TestCases.cpp -lgtest -lgtest_main -pthread -o TestRun ``` 7. Now, run the binary with the following command: ```cpp ./TestRun ``` 这显示了通过**检查点**和**测试套件**的两个测试用例。第一个测试用例**被调用并且通过。第二个测试用例, **CheckIp。BadLength** ,被称为,它也通过。这个结果可以在下面的截图中看到:** ![Figure 7.11: Compiling and executing test cases](img/C14583_07_11.jpg) ###### 图 7.11:编译和执行测试用例 #### 注意 在`谷歌测试`中,我们也可以使用其他断言,但是对于我们的测试用例,我们可以使用`ASSERT_FALSE`,因为我们只检查我们通过的 IP 地址的假条件。 8. Now, we will add more test cases to make our code robust. This is generally good practice for writing code. First, create the test cases and make sure the code runs fine for new test cases and old test cases along with the correct functionality of the code. To add more test cases, add the following code to the **TestCases.cpp** file: ```cpp TEST(CheckIp, WrongTokenCount){     testIp = "22.13111.11";     ASSERT_FALSE(checkValidIp(testIp)); } TEST(CheckIp, WrongTokenEmpty){     testIp = "22.131..11";     ASSERT_FALSE(checkValidIp(testIp)); } TEST(CheckIp, WrongTokenStart){     testIp = ".2.1.31.11";     ASSERT_FALSE(checkValidIp(testIp)); } TEST(CheckIp, WrongTokenEnd){     testIp = "2.13.11.1.";     ASSERT_FALSE(checkValidIp(testIp)); } TEST(CheckIp, SpaceToken){     testIp = "2.13.11\. 1";     ASSERT_FALSE(checkValidIp(testIp)); } TEST(CheckIp, NonDigit){     testIp = "2.13.b1.A1";     ASSERT_FALSE(checkValidIp(testIp)); } TEST(CheckIp, NonValidDigit){     testIp = "2.13.521.61";     ASSERT_FALSE(checkValidIp(testIp)); } TEST(CheckIp, CorrectIp){     testIp = "232.13.123.1";     ASSERT_FALSE(checkValidIp(testIp)); } ``` 在前面的代码中,第一种和第二种情况应该会因不正确的令牌而失败。如果`IP`以“”开头,第三种情况应该会失败。如果`IP`以“.”结尾,第四种情况应该会失败。如果`IP`有中间空间,第五种情况应该会失败。如果`IP`包含任何非数字字符,第六种情况应该会失败。如果`IP`的令牌值小于`0`且大于`255`,则第七种情况应该会失败。如果`IP`令牌计数错误,最后一种情况应该会失败。 9. 现在,在 **CheckIp.cpp** 文件的`CheckValidIp()`函数中添加以下代码。处理新的测试用例需要这个代码: ```cpp if(ip[strlen(ip)-1] == '.'){     cout<<"ERROR : Incorrect token at end"< tokens; string token; regex expression("[^0-9]"); smatch m; while(getline(istrstr, token, '.')){     if(token.empty()){         cout<<"ERROR : Got empty token"<255){         cout<<"ERROR : Invalid digit in token"< #include #include #include #include #include using namespace std; ``` 2. 创建一个名为`的连接数据库`的类,它将连接到数据库并返回查询结果。在类中,声明`数据库名`、用户和密码变量。另外,声明一个构造函数和两个虚函数。这两个虚函数中,第一个必须是析构函数,第二个必须是`getResult()`函数,从数据库返回查询结果。添加以下代码来实现: ```cpp class ConnectDatabase{     string DBname;     string user;     string passwd;     public:         ConnectDatabase() {}         ConnectDatabase(string _dbname, string _uname, string _passwd) :             DBname(_dbname), user(_uname), passwd(_passwd) { }         virtual ~ConnectDatabase() {}         virtual string getResult(string query); }; ``` 3. Create another class named `WebServerConnect`. Declare three `string` variables inside the `class`, namely `Webserver`, `uname`, and `passwd`. Create constructor and two virtual functions. Out of these two virtual functions, the first one must be a destructor, and the second one must be the `getRequest()` function. Add the following code to implement this: ```cpp class WebServerConnect{     string Webserver;     string uname;     string passwd;     public :     WebServerConnect(string _sname, string _uname, string _passwd) :             Webserver(_sname), uname(_uname), passwd(_passwd) { }         virtual ~WebServerConnect() {}         virtual string getRequest(string req); }; ``` #### 注意 `虚拟函数`是必需的,因为我们要从前一个类创建一个`模拟类`并调用这些函数。 4. Create a class named `App`. Create the constructors, and destructors and call all the functions. Add the following code to implement this: ```cpp class App {     ConnectDatabase *DB;     WebServerConnect *WB;     public :         App():DB(NULL), WB(NULL) {}         ~App() {             if ( DB )  delete DB;             if ( WB )  delete WB;         }         bool checkValidIp(string ip);         string getDBResult(string query);         string getWebResult(string query);         void connectDB(string, string, string);         void connectDB(ConnectDatabase *db);         void connectWeb(string, string, string);         void run(); }; ``` 在前面的代码中,app 会先查询数据库,得到`的 IP 地址`。然后,它用必要的信息连接到 web 服务器,并查询它以获得所需的信息。 5. Create a class named **MockMisc.h** and add the following code: ```cpp #include"Misc.h" #include #include class MockDB : public ConnectDatabase {     public :         MockDB() {}         virtual ~MockDB(){}         MOCK_METHOD1(getResult, string( string) ); }; ``` 在前面的代码中,您可以看到我们已经包含了`gmock`头文件,这是创建一个模拟类所需要的。此外,`MockDB`类继承自`ConnectDatabase`类。`MOCK_METHOD1(getResult,string(字符串));`行表示我们要模拟`获取结果`界面。因此,在单元测试过程中,我们可以直接用想要的结果调用`getResult`函数,而不需要创建`ConnectDatabase`类和运行对数据库的真实查询。这里需要注意的重要一点是,我们需要模拟的函数必须用`MOCK_METHOD[N]宏`来定义,其中 N 是接口将要取的参数个数。在我们的例子中,`获取结果`界面接受一个参数。因此,使用`MOCK_METHOD1`宏对其进行模拟。 6. Create a file named **Misc.cpp** and add the following code: ```cpp #include"Misc.h" #include string ConnectDatabase::getResult(string query){     // dummy func, need to implement..     // assuming query sent to DB is success and     // will return some dummy string     return string("DB returned success"); } string WebServerConnect::getRequest(string req){     // dummy func, need to implement..     // assume no req string is sent to webserver. .     // its returns the result returned from server.     return string("Webserver returned success"); } void App::connectDB(string dbname, string user, string passwd){     if ( DB )         delete DB;     DB = new ConnectDatabase(dbname, user, passwd); } void App::connectDB(ConnectDatabase *db){     if ( DB )         delete DB;     DB = db; } void App::connectWeb(string webname, string user, string passwd){     if ( WB )         delete WB;     WB = new WebServerConnect(webname, user, passwd); } string App::getDBResult(string query){     return DB->getResult(query); } string App::getWebResult(string query) {     return WB->getRequest(query); } void App::run(){     if ( (DB == NULL) || (WB == NULL) )         return ;     while( true ){         // read some request to be run on web and get result. ..         cout << getWebResult("dummy request to webserver") << endl;;         sleep(5);     } } bool App::checkValidIp(string ip){     if(ip.empty()){         cout << "ERROR : IP passed is NULL " << endl;         return false;     }     if(ip.size() > 15){         cout << "ERROR : IP size is greater than 15" < tokens;     string token;     regex expression("[^0-9]");     smatch m;     while( getline(istrstr, token, '.') ){         if ( token.empty() ){             cout << "ERROR : Got empty token " << endl;             return false;         }         if ( token.find(' ') != string::npos){             cout << "ERROR : Space character in token " << endl;             return false;         }         if ( regex_search(token, m, expression) ){             cout << "ERROR : NonDigit character in token " << endl;             return false;         }         int val = atoi(token.c_str());         if ( val < 0 || val > 255 ){             cout << "ERROR : Invalid Digit in token " << endl;             return false;         }         tokens.push_back(token);     }     if ( tokens.size() != 4 ){ cout << " ERROR : Incorect IP tokens used" << endl; return false;     }     cout << "strlen: " << ip.size() << endl;     return true; } ``` 在前面的代码中,我们创建了一个最小的接口和虚拟参数来运行它,这样我们就可以理解实际的功能。我们已经为`getResult()`和`getRequest()`函数开发了基本功能,其中数据库查询和`网络服务器`查询返回一个默认字符串。这里`App::run()`函数假设数据库连接和 web 服务器连接都已经执行,现在可以定期执行 web 查询。在每次查询结束时,默认会返回“`Webserver 返回成功`”字符串。 7. Now, create a file named **RunApp.cpp** and write the following code inside the main function: ```cpp #include"Misc.h" int main(){     App app;     app.connectDB("dbname","dbuser", "dbpasswd");     string ip = app.getDBResult("dummy");     // DB query to get Webserver IP     // Similarly some miscellaneous activities to get configuratio information     // Like querying DB to get correct username/passwd to connect to WebServer..     // After getting IP from DB, check if the IP is valid..     //app.checkValidIp(ip);     // Now conect to webserver with parameters extracted from DB.     app.connectWeb("webname","user", "passwd");     // Now run the App, like sending some request to webserver,     // getting result and doing activity with received data.     app.run();     return 0; } ``` 正如您在前面的代码中看到的,创建了应用类实例。使用这个实例,我们借助虚拟参数连接到数据库,即`数据库名`、`数据库用户`和`数据库密码`。然后,我们查询数据库以获取 IP 地址和其他配置参数。我们已经评论了`app.checkValidIp(ip)`行,因为我们假设我们从数据库获取的 Ip 地址需要验证。此外,该功能需要进行单元测试。使用`connectWeb()`功能,我们可以通过传递`网名`、`用户`、`passwd`等伪参数来连接 Web 服务器。最后,我们调用`run()`函数,该函数将在迭代中运行,从而查询 web 服务器并给出默认输出。 8. Save all the files and open the terminal. In order to get the basic functionality required to execute the project, we'll build the binary file and execute it to see the result. Run the following command in the terminal: ```cpp g++ Misc.cpp RunApp.cpp -o RunApp ``` 前面的代码将在当前文件夹中创建名为`RunApp`的二进制文件。 9. Now, write the following command to run the executable: ```cpp ./RunApp ``` 前面的命令在终端中生成以下输出: ![Figure 7.13: Running the app](img/C14583_07_13.jpg) ###### 图 7.13:运行应用 正如您在前面的截图中看到的,二进制文件及时显示了输出“`网络服务器返回成功`”。到目前为止,我们的应用运行良好,因为它假设所有接口都如预期的那样工作。但是我们仍然需要测试一些功能,例如验证`IP 地址`、`数据库连接`、检查`用户名`和`密码`的格式是否正确等等,然后才能为`QA`做准备。 10. Using the same infrastructure, start unit testing each functionality. For our exercise, we'll assume that the `DB connectivity` has already been done and has been queried to get the `IP address`. After that, we can start unit testing the validity of the `IP address`. So, in our test case, the Database class needs to be mocked and the `getDBResult` function must return the `IP address`. This `IP address` will be passed to the `checkValidIP` function later, wherein we'll test it. To implement this, create a class named **TestApp.cpp** wherein we'll be calling the `checkValidIP` function: ```cpp #include"MockMisc.h" using ::testing::_; using ::testing::Return; class TestApp : public ::testing::Test {     protected :         App testApp;         MockDB *mdb;         void SetUp(){             mdb = new MockDB();             testApp.connectDB(mdb);         }         void TearDown(){         } }; TEST_F(TestApp, NullIP){     EXPECT_CALL(*mdb, getResult(_)).                  WillOnce(Return(""));     ASSERT_FALSE(testApp.checkValidIp(testApp.getDBResult(""))); } TEST_F(TestApp, SpaceTokenIP){     EXPECT_CALL(*mdb, getResult(_)).                  WillOnce(Return("13\. 21.31.68"));     ASSERT_FALSE(testApp.checkValidIp(testApp.getDBResult(""))); } TEST_F(TestApp, NonValidDigitIP){     EXPECT_CALL(*mdb, getResult(_)).                  WillOnce(Return("13.521.31.68"));     ASSERT_FALSE(testApp.checkValidIp(testApp.getDBResult(""))); } TEST_F(TestApp, CorrectIP){     EXPECT_CALL(*mdb, getResult(_)).                  WillOnce(Return("212.121.21.45"));     ASSERT_TRUE(testApp.checkValidIp(testApp.getDBResult(""))); } ``` 在这里,我们使用了测试和`测试::返回`名称空间来调用模拟类接口,并返回用户定义的值,这些值用于测试用例。在`TEST_F`函数中,我们使用了`EXPECT_CALL`函数,其中我们将模拟对象的实例作为第一个参数传递,将`getResult()`函数作为第二个参数传递。`will once(Return(" ")`行声明接口需要调用一次,并将返回“”和一个空字符串。这是需要传递给`checkValidIP`函数来测试空字符串的值。这是用`ASSERT_FALSE`宏检查的。同样,可以使用数据库的模拟对象创建其他测试用例,并将 IP 地址传递给`checkValidIP`函数。为了创建各种测试用例,`Test PP`类继承自`testing::Test`类,它包含数据库的 App 实例和模拟对象。在`TestApp`类中,我们定义了两个函数,即`SetUp()`和`deterdown()`。在`SetUp()`函数中,我们创建了一个`MockDB`实例,并将其标记到 testApp 实例中。由于`拆卸()`功能不需要做任何事情,所以我们将它保留为空。其析构函数在`App`类的析构函数中调用。另外,我们在`TEST_F`函数中传递了两个参数。第一个参数是测试类,而第二个参数是测试用例的名称。 11. Save all the files and open the terminal. Run the following command: ```cpp g++ Misc.cpp TestApp.cpp -lgtest -lgmock -lgtest_main -pthread -o TestApp ``` 在前面的命令中,我们还链接了`gmock 库`。现在,键入以下命令来运行测试用例: ```cpp ./TestApp ``` 前面的命令生成以下输出: ![Figure 7.14: Running the Gmock test](img/C14583_07_14.jpg) ###### 图 7.14:运行 Gmock 测试 从前面的命令中,我们可以看到所有的测试用例都成功地执行并通过了。在下一节中,我们将讨论`断点`、`观察点`和`数据可视化`。 ### 断点、观察点和数据可视化 在前一节中,我们讨论了单元测试需要在开发人员将代码签入存储库分支之前完成,并且可以被其他团队成员看到,以便他们可以将其与其他模块集成。尽管单元测试做得很好,并且开发人员签入了代码,但是无论何时代码被集成并且质量保证团队开始测试,他们都有可能发现代码中的错误。通常,在这种情况下,由于另一个模块中的更改,可能会在该模块中引发错误。对于团队来说,打击这些问题的真正原因可能会变得很困难。在这种情况下,**调试**进入画面。它确切地告诉我们代码的行为,开发人员可以获得代码执行的粒度信息。开发人员可以看到函数得到了什么参数,返回了什么值。它可以准确地告诉我们变量或指针的赋值,或者内存中的内容。这对于开发人员识别问题出在代码的哪个部分变得非常有帮助。在下一节中,我们将实现一个堆栈,并对其执行一些操作。 ### 使用堆栈数据结构 考虑一个场景,开发人员被要求开发他/她自己的堆栈结构,该结构可以接受任何参数。这里的要求是堆栈结构必须遵循**后进先出** ( **后进先出**)原则,其中元素被放置在彼此之上,当它们从堆栈中移除时,最后一个元素应该首先被移除。它应该具有以下功能: * **按()**将新元素放在堆栈顶部 * **top()** 显示堆栈的顶部元素(如果有) * **pop()** 从堆栈中移除最后插入的元素 * **为 _empty()** 检查堆栈是否为空 * **size()** 显示堆栈中存在的元素数量 * **清除()**清空堆栈(如果其中有任何元素) 下面几行代码展示了如何在 **Stack.h** 头文件中包含必要的库: ```cpp #ifndef STACK_H__ #define STACK_H__ #include using namespace std; ``` 正如我们已经知道的,堆栈由各种操作组成。为了定义这些函数,我们将编写以下代码: ```cpp template struct Node{     T element;     Node *next; }; template class Stack{     Node *head;     int sz;     public :         Stack():head(nullptr), sz(0){}         ~Stack();         bool is_empty();         int size();         T top();         void pop();         void push(T);         void clean(); }; template Stack::~Stack(){     if ( head ) clean(); } template void Stack::clean(){     Node *tmp;     while( head ){         tmp = head;         head = head -> next;         delete tmp;         sz--;     } } template int Stack::size(){     return sz; } template bool Stack::is_empty(){         return (head == nullptr) ? true : false; } template T Stack::top(){     if ( head == nullptr){         // throw error ...         throw(string("Cannot see top of empty stack"));     }else {         return head -> element;     } } template void Stack::pop(){     if ( head == nullptr ){         // throw error         throw(string("Cannot pop empty stack"));     }else {         Node *tmp = head ;         head = head -> next;         delete tmp;         sz--;     } } template void Stack::push(T val){     Node *tmp = new Node();     tmp -> element = val;     tmp -> next = head;     head = tmp;     sz++ ; } // Miscellaneous functions for stack.. template void displayStackStats(Stack &st){     cout << endl << "------------------------------" << endl;     cout << "Showing Stack basic Stats ...  " << endl;     cout << "Stack is empty : " << (st.is_empty() ? "true" : "false") << endl;     cout << "Stack size :" << st.size() << endl;     cout << "--------------------------------" << endl << endl; } #endif ``` 到目前为止,我们已经看到了如何使用`单链表`实现堆栈。每次在堆栈中调用`推送`时,都会创建一个给定值的新元素并附加到堆栈的开头。我们称之为 head 成员变量,它是 head 指向堆栈中下一个元素的地方,以此类推。当调用`pop`时,头部将从堆栈中移除,并指向堆栈的下一个元素。 让我们在 **Main.cpp** 文件中编写之前创建的 Stack 的实现。主函数有一个 try 块,用于创建整数堆栈和字符堆栈。两者都有一些推送和弹出,在这两者之间,调用堆栈的顶部来显示最新的元素。对于一堆整数,开头涉及三次推送:`22`、`426`、`57`。调用`displayStackStats()`函数时,应声明堆栈大小为`3`。然后,我们从堆栈中弹出`57`,顶部元素必须显示`426`。我们将对`char`的堆栈进行同样的操作。下面是堆栈的完整实现: ```cpp #include"Stack.h" int main(){     try {         Stack si;         displayStackStats(si);         si.push(22);         si.push(426);         cout << "Top of stack contains " << si.top() << endl;         si.push(57);         displayStackStats(si);         cout << "Top of stack contains " << si.top() << endl;         si.pop();         cout << "Top of stack contains " << si.top() << endl;         si.pop();         displayStackStats(si);         Stack sc;         sc.push('d');         sc.push('l');         displayStackStats(sc);         cout << "Top of char stack contains:" << sc.top() << endl;     }     catch(string str){         cout << "Error : " << str << endl;     }     catch(...){         cout << "Error : Unexpected exception caught " << endl;     }     return 0; } ``` 当我们通过编写以下命令编译 **Main.cpp** 文件时,Main 可执行文件将在调试模式下创建(因为使用了`-g`选项)。因此,如果需要,您可以调试二进制文件: ```cpp g++ -g Main.cpp -o Main ``` 我们将编写以下命令来执行二进制文件: ```cpp ./Main ``` 前面的命令生成以下输出: ![Figure 7.15: Main function using the Stack class](img/C14583_07_15.jpg) ###### 图 7.15:使用堆栈类的主函数 在前面的输出中,对 statistics 函数的第二次调用中的红墨水显示了在 int 堆栈中显示三个元素的正确信息。但是,对 int 堆栈顶部的红墨水调用显示随机值或垃圾值。如果程序再次运行,会显示一些其他随机数,而不是`57`和`426`的期望值。同样,对于 char 的栈,用红墨水突出显示的部分,即`char`的顶部,显示的是一个垃圾值,而不是期望值,即“l”。后来,执行显示双重自由或损坏的错误,这意味着自由被再次调用到同一个内存位置。最后,可执行文件给出了核心转储。程序没有按预期执行,从显示屏上可能看不出实际错误在哪里。要调试`Main`,我们将编写以下命令: ```cpp gdb ./Main ``` 前面的命令生成以下输出: ![Figure 7.16: Debugger display – I](img/C14583_07_16.jpg) ###### 图 7.16:调试器显示-I 在前面的截图中,以蓝色突出显示的标记显示了调试器的使用方式和显示内容。第一个标记显示使用`gdb`命令调用调试器。进入`gdb`命令后,用户进入调试器的命令模式。以下是命令模式中使用的命令的一些简要信息: * **b main** :这告诉调试器在主函数调用时中断。 * **r** :它是用来运行可执行文件的简短形式。它也可以通过传递参数来运行。 * **n** :是 next 命令的简写形式,告诉我们执行下一条语句。 * **观察 si** :当在代码中调用`si`变量时,它的值发生变化。调试器将显示使用该变量的代码内容。 * **s** :是命令中“**步”的简称。** 下一个要执行的语句是`si.push(22)`。由于`si`已经更新,观察点调用并显示`si`的旧值和`si`的新值,其中显示`si`的旧值为空,且`sz`为 0。在`si.push`之后,头部被更新为新值,其执行到达`Stack.h`文件的第 75 行,这是`sz`变量递增的地方。如果再次按下*回车*键,将执行。 注意,执行已经自动从主功能移到`栈::推`功能。下面是调试器上继续执行的命令的屏幕截图: ![](img/C14583_07_17.jpg) ###### 图 7.17:调试器显示–二 下一个命令显示`sz`已经更新为新值`1`。按下*进入*后,代码的执行从`栈:将**第 76 行**`**上的**推回到第 8 行的主功能。这在下面的截图中突出显示。显示执行在`si.push(426)`调用时停止。一旦我们介入,就会调用`栈::推`。执行移动到`Stack.h`程序的`第 71 行`,如红墨水所示。一旦执行到`第 74 行`,如红墨水所示,手表被调用,显示`si`被更新为新值。可以看到`栈::推`功能完成后,流程回到主代码。以下是在调试器中执行的步骤的屏幕截图: ![](img/C14583_07_18.jpg) ###### 图 7.18:调试器显示–三 按下*进入*后,你会看到`显示状态`在`第 11 行`被调用。在 **Main.cpp** 程序中,下一个命令显示堆栈的顶部元素,该元素在`第 12 行`被调用。但是显示屏显示的数值为`0`,而不是`57`的期望值。这是一个我们仍然无法理解的错误——为什么值会改变?但是,很明显,在前面对主函数的调用中,该值可能已经发生了变化。因此,这可能不会让我们对继续前面的调试感兴趣。但是,我们需要从头开始进行调试。 下面的屏幕截图显示了将用于调试代码的命令: ![](img/C14583_07_19.jpg) ###### 图 7.19:调试器显示–四 要从头重新运行程序,必须按 *r* ,确认并继续,需要按 *y* ,也就是从头重新运行程序。它会要求确认;按下 *y* 继续。在前面的截图中,所有这些命令都以蓝色突出显示。在第 7 行的执行过程中,我们需要运行“`display *si.head`”命令,该命令将在执行的每条语句之后持续显示`si.head`存储单元的内容。如红墨水所示,将`22`推到堆栈上后,头会更新为正确的值。同样,对于值`426`和`57`,当使用 push 插入堆栈时,对 head 的调用被正确更新。 后来调用`displayStackStats`时,显示的是`3`的正确`尺寸`。但是当调用 top 命令时,头部显示的值是错误的。这用红墨水突出显示。现在,top 命令的代码没有改变 head 的值,所以很明显在前面的执行语句中出现了一个错误,即在`displayStackStats`处。 因此,我们缩小了可能存在问题的代码的范围。我们可以运行调试器来指向`显示堆栈状态`并移动到`显示堆栈状态`中,以查找导致堆栈中的值被更改的原因。下面是其中的屏幕截图,用户需要从头开始启动调试器: ![Figure 7.20: Debugger display – IV](img/C14583_07_20.jpg) ###### 图 7.20:调试器显示–四 从头开始重启调试器,到达调用`displayStackStats`的第 11 行执行点后,我们需要介入。流程是进入`显示状态`功能的开始。另外,我们需要执行下一条语句。由于函数中的初始检查是清晰的,它们不会改变头部的值,我们可以按*进入*执行下一步。当我们怀疑接下来的步骤会改变我们正在寻找的变量的值时,我们需要介入。这是在前面以红色突出显示的快照中完成的。后者执行到`第 97 行`,即`显示堆栈状态`功能的最后一行。 在输入 *s* 后,执行移动到析构函数堆栈,并在第 81 行调用 clean 函数。这个清除命令删除了 **tmp** 变量,该变量的值与头部的值相同。该函数清空堆栈,这是不希望运行的。只有 **displayStackStats** 函数应该被调用并执行,以最终返回到主函数。但是析构函数可能会因为函数中的局部变量在函数完成后超出范围而被调用。这里,局部变量是在第 92 行被声明为 **displayStackStats** 函数参数的变量。因此,当调用 **displayStackStats** 函数时,创建了主函数中 **si** 变量的本地副本。这个变量在超出范围时调用堆栈的析构函数。现在 **si** 变量的指针已经被复制到临时变量中,并错误地删除了末尾的指针。这不是开发人员的本意。因此,在代码执行结束时,出现了双重自由错误。 **si** 变量在超出范围时必须调用堆栈析构函数,因为它将再次尝试释放相同的内存。要解决这个问题,很明显 **displayStackStats** 函数必须以传递参数作为引用来调用。为此,我们必须更新**堆栈. h** 文件中**显示堆栈状态**函数的代码: ```cpp template void displayStackStats(Stack &st){     cout << endl << "------------------------------" << endl;     cout << "Showing Stack basic Stats ...  " << endl;     cout << "Stack is empty : " << (st.is_empty() ? "true" : "false") << endl;     cout << "Stack size :" << st.size() << endl;     cout << "--------------------------------" << endl << endl; } ``` 现在,当我们保存并编译 **Main.cpp** 文件时,将生成二进制文件: ```cpp ./Main ``` 前面的命令在终端中生成以下输出: ![Figure 7.21: Debugger display – IV](img/C14583_07_21.jpg) ###### 图 7.21:调试器显示–四 从前面的截图中,我们可以看到`57`和`426`的期望值显示在栈顶。`显示堆栈状态`功能还显示 int 和 char Stack 的正确信息。最后,我们使用调试器找到了错误并修复了它。在下一节中,我们将解决一个活动,其中我们将开发解析文件的函数,并编写测试用例来检查函数的准确性。 ### 活动 1:使用测试用例检查功能的准确性并理解测试驱动开发(TDD) 在本练习中,我们将开发函数,这样我们就可以解析文件,然后编写测试用例来检查我们开发的函数的正确性。 大型零售组织中的一个信息技术团队希望通过将产品详细信息和客户详细信息存储在其数据库中来跟踪产品销售,作为其对账的一部分。销售部门将定期以简单的文本格式向信息技术团队提供这些数据。作为开发人员,您需要确保数据的基本健全性检查已经完成,并且在公司将记录存储到数据库之前,所有记录都被正确解析。销售部门将提供两个文本文件,保存所有销售交易的客户信息和货币信息。您需要编写解析函数来处理这些文件。这两个文件分别是 **RecordFile.txt** 和 **CurrencyConversion.txt** 。 **RecordFile.txt** 文件包含客户的详细信息、他们购买的产品以及以地区货币和外币表示的总价。 **CurrencyConversion.txt** 文件包含两个字段,即`币种`和`ConversionRatio`。 该项目环境设置的所有必要信息都保存在配置文件中。这还将保存文件名,以及其他参数(如`数据库`、`RESTAPI`等)和一个名为 **parse.conf** 的文件中的变量值。每行包含两个字段,由分隔符“=”分隔。第一行将是一个标题,说明“配置文件”。对于记录文件,变量名为`记录文件`,同样对于货币文件,变量名为`货币文件`。 以下是我们将要编写的测试条件,用于检查解析 **CurrencyConversion.txt** 文件的函数的准确性: * 第一行应该是标题行,它的第一个字段应该包含“`Currency`”字符串。 * `货币`字段应由三个字符组成。例如:“`美元`”、“`英镑`有效。 * `转换比率`字段应该由浮点数组成。例如:`1.2`、`0.06`有效。 * 每行应该正好有两个字段。 * 用于记录的分隔符是“|”。 下面是我们将要编写的测试条件,用来检查用来解析 **RecordFile.txt** 文件的函数的准确性: * 第一行应包含标题行,其第一个字段应包含“`客户标识`”字符串。 * `客户号`、`订单号`、`产品号`、`数量`都应该是整数值。例如`12312`、`4531134`有效。 * `合计价格(地区货币)``合计价格(美元)`应为浮点值。例如:`2433.34`、`3434.11`有效。 * `地区货币`字段的值应该出现在**货币转换. txt** 文件或`标准::地图`中。 * 每行应该正好有九个字段,如文件的`HEADER`信息中所定义的。 * 记录的分隔符是“|”。 按照以下步骤实施本活动: 1. 解析 **parse.conf** 配置文件,其中包含项目运行的环境变量。 2. 从步骤 1 开始,正确设置`记录文件`和`当前文件`变量。 3. 使用我们从配置文件中检索到的这些变量,解析满足所有条件的货币文件。如果不满足条件,则返回适当的错误消息。 4. 用我们满足的所有条件解析记录文件。如果没有,则返回错误消息。 5. 创建一个名为`CommonHeader.h`的头文件,并声明所有的实用函数,即`isAllNumbers()`、`isDigit()`、`parceline()`、`checkFile()`、`parseConfig()`、`parsecurrency parameters()`、`fillCurrencyMap()`、`recordparsefile()`、【 6. 创建一个名为`Util.cpp`的文件,定义所有的实用函数。 7. 创建一个名为`ParseFiles.cpp`的文件,调用`parseConfig()`、`filllcurrency map()`和`parseRecordFile()`函数。 8. 编译并执行`Util.cpp`和`ParseFiles.cpp`文件。 9. 创建一个名为`ParseFileTestCases.cpp`的文件,为函数编写测试用例,即`trim()`、`isAllNumbers()`、`isDigit()`、`parsecurrenceparameters()`、`checkFile()`、`parseConfig()`、`filecorency map()`和`parseRecordFile()` 10. 编译并执行`Util.cpp`和`ParseFileTestCases.cpp`文件。 以下是解析不同文件和显示信息的流程图: ![](img/C14583_07_22.jpg) ###### 图 7.22:流程图 从上面的流程图中,我们大致了解了执行流程。为了在编写代码之前有一个清晰的理解,让我们看看更精细的细节。它将有助于为每个执行块定义测试用例。 为了解析配置文件块,我们可以将步骤分为以下几个部分: 1. 检查配置文件是否存在并具有读取权限。 2. 检查它是否有适当的标题。 3. 逐行分析整个文件。 4. 对于每一行,解析以“=”作为分隔符的字段。 5. 如果上一步有 2 个字段,处理看是`货币文件`还是`记录文件`变量并适当存储。 6. 如果步骤 4 中没有 2 个字段,请转到下一行。 7. 完全解析文件后,检查上述步骤中的两个变量是否都不为空。 8. 如果为空,返回错误。 为了解析`货币文件`块,我们可以将步骤分解为以下内容: 1. 读取`CurrencyFile`的变量,看该文件是否存在,是否有读取权限。 2. 检查它是否有适当的标题。 3. 用“|”作为分隔符,逐行分析整个文件。 4. 如果每行正好有 2 个字段,可以考虑第一个作为`货币字段`,第二个作为`换算字段`。 5. 如果在步骤 3 中没有找到 2 个字段,则返回相应的错误消息。 6. 从第 4 步开始,对`币种字段`(应为 3 个字符)和`转换字段`(应为数字)进行所有检查。 7. 如果从步骤 6 通过,将`货币` / `转换`值成对存储在地图上,键为`货币`,值为数字。 8. 如果步骤 6 没有通过,返回错误说明`货币`。 9. 在完成对`货币`文件的解析后,将创建一个包含所有货币的转换值的地图。 对于解析`记录文件`块,我们可以将步骤分解为以下内容: 1. 读取`记录文件`的变量,查看文件是否存在,是否有读取权限。 2. 检查它是否有适当的标题。 3. 用“|”作为分隔符,逐行分析整个文件。 4. 如果在上述步骤中没有找到 9 个字段,则返回相应的错误消息。 5. 如果找到 9 个字段,对“活动开始”中列出的所有字段进行相应的检查。 6. 如果步骤 5 没有通过,返回适当的错误消息。 7. 如果步骤 5 通过,将记录存储在记录向量中。 8. 在完全解析记录文件之后,所有的记录将被存储在记录向量中。 在创建解析所有三个文件的流程时,我们看到对所有三个文件重复的步骤很少,例如: 检查文件是否存在且可读 检查文件是否有正确的标题信息 用分隔符解析记录 检查字段是否为`数字`在`货币`和`记录文件`中常见 检查字段是否为`数字`在`货币`和`记录文件`中很常见 以上几点将有助于重构代码。此外,还有一个解析带分隔符字段的通用函数,即`修剪函数`。因此,当我们用分隔符解析记录时,我们可以在开头或结尾用空格或制表符获取值,这可能是不需要的,所以我们需要在解析记录时修剪一次。 现在我们知道我们有以上常见的步骤,我们可以为它们编写单独的函数。从 TDD 开始,我们首先了解功能需求,然后开始编写单元测试用例来测试这些功能。然后我们编写函数,这样它将通过单元测试用例。如果很少有测试用例失败,我们就重复更新函数和执行测试用例的步骤,直到它们全部通过。 对于我们的例子,上面我们可以写`修剪`函数, 现在我们知道在修剪功能中,我们需要删除第一个和最后一个多余的空格/制表符。例如,如果字符串包含“AA”,修剪应该返回“AA”删除所有空格。 trim 函数可以返回带有预期值的新字符串,也可以更新传递给它的相同字符串。 所以现在我们可以写 trim 函数的签名:`字符串 trim(string&);` 我们可以为它编写以下测试用例: * 如果只有多余的字符(" "),则返回空字符串()。 * 开头只有空字符(“AA”)的返回字符串带有结尾字符(“AA”) * 结尾只有空字符(“AA”),应该返回开头有字符(“AA”)的字符串 * 中间带字符(“AA”)返回带字符(“AA”)的字符串 * 中间有空格(“AA BB”),返回相同的字符串(“AA BB”) * 单个字符的所有步骤 3、4、5。应该返回单个字符的字符串。 要创建测试用例,请检查文件 **ParseFileTestCases.cpp** ,用于`trim`功能的测试用例写在测试套件`trim`中。现在写 **Util.cpp** 文件(所有杂项功能都写在 **Util.cpp** 中)。用文件中显示的签名编写`修剪`功能。执行`微调`功能的测试用例,检查是否通过。它没有适当地改变功能并再次测试它。重复直到所有测试用例通过。 现在我们有信心在项目中使用`修剪`功能。对其余的常用功能重复类似的步骤(`isDigit`,`isNumeric`,`parseHeader`等等)。请参考 **Util.cpp** 文件和 **ParseFiletestCases.cpp** 文件,测试所有常用功能。 完成常用函数后,我们可以分别编写解析每个文件的函数。这里要理解和学习的主要内容是如何将模块分解成小函数。找到重复的小任务,为每个任务创建小函数,以便重构。理解这些小函数的详细功能,并创建适当的单元测试用例。 完成单个函数并彻底测试它,如果失败,那么更新该函数直到它通过所有测试用例。同样,完成其他功能。然后为更大的函数编写和执行测试用例,这应该相对容易,因为我们在这些更大的函数中调用上面测试的小函数。 在实现了前面的步骤之后,我们将获得以下输出: ![Figure 7.23: All tests running properly](img/C14583_07_23.jpg) ###### 图 7.23:所有测试运行正常 以下是后续步骤的截图: ![Figure 7.24: All tests running properly](img/C14583_07_24.jpg) ###### 图 7.24:所有测试运行正常 #### 注意 这个活动的解决方案可以在第 706 页找到。 ## 总结 在这一章中,我们研究了在编译时和运行时使用断言获取可执行文件抛出的错误的各种方法。我们还学习了静态断言。我们了解异常是如何生成的,以及如何在代码中处理它们。我们还看到了单元测试如何成为开发人员的救星,因为他们将能够在开始时识别代码中的任何问题。我们为需要在测试用例中使用的类使用了模拟对象。然后,我们了解了调试器、断点、观察点和可视化数据。我们能够使用调试器找到代码中的问题并修复它们。我们还解决了一个活动,其中我们编写了必要的测试用例来检查用于解析文件的函数的准确性。 在下一章,我们将学习如何优化我们的代码。我们将回顾处理器如何执行代码和访问内存。我们还将学习如何确定软件执行所需的额外时间。最后,我们将了解内存对齐和缓存访问。 ================================================ FILE: docs/adv-cpp/09.md ================================================ # 九、对速度的需求——性能和优化 ## 学习目标 本章结束时,您将能够: * 手动为代码性能计时 * 使用源代码检测来测量代码执行时间 * 使用性能工具分析程序性能 * 使用 godbolt 编译器资源管理器工具来分析编译器生成的机器代码 * 使用编译器标志来生成更好的代码 * 应用能够提高性能的代码习惯用法 * 编写缓存友好的代码 * 将算法级优化应用于现实问题 在这一章中,我们将探索一些概念,这些概念将使我们能够编写快速的代码,以及一些特别适用于 C++ 的实用技术。 ## 简介 在当今这个软件系统极其庞大和复杂的世界里,`稳定性`和`可维护性`通常被认为是大多数软件项目的主要目标,然而自 2000 年代以来,优化并没有被广泛认为是一个有价值的目标。这是因为硬件技术的快速发展超过了软件需求。 多年来,硬件的改进似乎将继续跟上软件的性能需求,但应用继续变得更大、更复杂。与性能较低但更容易使用解释语言(如`Python`或`Ruby`)相比,C 和 C++ 等低级本机编译语言的受欢迎程度有所下降。 然而,到 2000 年代末,每 18 个月 CPU 晶体管数量(和性能)翻一番的趋势(摩尔定律`的结果`已经停止,性能的提高已经趋于平缓。由于物理和制造成本的限制,5 到 10 千兆赫处理器在 2010 年代广泛可用的期望从未实现。然而,移动设备的快速采用以及用于数据科学和机器学习的高性能计算应用的兴起,突然复活了对快速高效代码的需求。随着大型数据中心消耗大量电力,性能功耗比已成为新的衡量标准。例如,2017 年,美国的谷歌服务器用电量超过了整个英国。 到目前为止,在本书中,我们已经了解了 C++ 语言在易用性方面是如何发展的,而没有牺牲任何优于传统语言(如 C)的性能潜力。这意味着我们可以用 C++ 编写快速代码,而不必牺牲可读性或稳定性。在下一节中,我们将学习性能测量的概念。 ## 性能测量 优化最重要的方面是代码执行时间的**测量。除非我们用广泛的输入数据集来衡量我们的应用的性能,否则我们将不知道哪个部分花费的时间最多,并且我们的优化工作将被蒙在鼓里,无法保证结果。有几种测量方法,这里列出了其中一些:** * 运行时检测或分析 * 源代码工具 * 手动执行时序 * 研究生成的汇编代码 * 通过研究使用的代码和算法进行手动估计 前面的列表是按照测量的精确度排序的(首先是最精确的)。然而,这些方法各有不同的优点。选择采用哪种方法取决于优化工作的目标和范围。为了尽可能快地实现,所有这些都是必需的。我们将在下面的章节中研究这些方法。 ### 手动估算 当我们用一个更好的算法替换一个算法时,性能可能得到最大的提高。例如,考虑一个平凡函数的两个版本,它将从`1`到`n`的整数相加: ```cpp int sum1(int n) {   int ret = 0;   for(int i = 1; i <= n; ++ i)   {     ret += i;   }   return ret; } int sum2(int n) {   return (n * (n + 1)) / 2; } ``` 第一个函数`sum1`使用一个简单的循环来计算总和,并且具有与`n`成比例的运行时复杂度,而第二个函数`sum2`使用代数求和公式,并且花费与`n`无关的恒定时间。在这个非常人为的例子中,我们简单地用代数的基本知识优化了一个函数。 有许多众所周知的算法,用于每一个可以想象的操作,已经被证明是最理想的。让我们的代码尽可能快地运行的最好方法是使用算法。 掌握算法词汇是很重要的。我们不需要成为算法专家,但我们至少需要意识到各种领域中高效算法的存在,即使我们没有能力从头实现它们。稍微深入一点的算法知识将有助于我们找到程序中与众所周知的算法执行相似(如果不是完全相同)计算的部分。某些代码特性,如嵌套循环或数据的线性扫描,通常是明显的改进候选,前提是我们可以验证这些构造在代码的热点内。一个**热点**是一段运行非常频繁且对性能影响很大的代码。C++ 标准库包含许多基本算法,可以用作构建块来提高许多常见操作的效率。 ### 研究生成的汇编代码 **汇编语言**是在处理器上实际执行的二进制机器代码的人类可读表示。对于任何认真学习 C++ 等编译语言的程序员来说,对汇编语言的基本理解是一笔巨大的财富。 研究为程序生成的汇编代码可以让我们对编译器如何工作以及代码效率的估计有一些很好的了解。在许多情况下,这是确定效率瓶颈的唯一可能方法。 除此之外,汇编语言的基本知识对于调试 C++ 代码是必不可少的,因为一些最难捕捉的错误是那些与低级生成代码相关的错误。 用于分析编译器生成的代码的一个非常强大和流行的在线工具是我们将在本章中使用的**编译器资源管理器**。 #### 注意 在[https://godbolt.org](https://godbolt.org)可以找到`戈德博尔特编译器浏览器`。 以下是 Godbolt 编译器浏览器的屏幕截图: ![](img/C14583_08_01.jpg) ###### 图 8.1:哥德堡编译器浏览器 如您所见,Godbolt 编译器资源管理器由两个窗格组成。左边的一个是我们输入代码的地方,而右边的一个显示生成的汇编代码。左侧窗格有一个下拉列表,以便我们可以选择所需的语言。出于我们的目的,我们将使用 C++ 语言和 gcc 编译器。 右侧窗格有一些选项,我们可以用来选择编译器版本。几乎所有流行编译器的版本都存在,比如`gcc`、`clang`、`cl` ( `微软 C++ `)包括那些针对 ARM 等非 X86 架构的编译器。 #### 注意 为了简单起见,我们将英特尔处理器架构称为`x86`,尽管正确的定义是`x86/64`。我们将省略提及“`64`”,因为今天制造的几乎所有处理器都是`64 位`。尽管`x86`是英特尔发明的,但现在所有的 PC 处理器制造商都获得了使用它的许可。 为了熟悉`编译器探索者工具`的基础知识,从基础层面理解`x86`汇编代码,我们来考察一下一个简单函数的编译器生成的汇编代码,这个简单函数将`1`到`N`的整数相加。下面是需要在编译器资源管理器的左侧窗格中编写的 sum 函数: ```cpp int sum(int n) {   int ret = 0;   for(int i = 1; i <= n; ++ i)   {     ret += i;   }   return ret; } ``` 在右侧窗格中,编译器必须设置为 **x86-64 gcc 8.3** ,如下所示: ![Figure 8.2: C++ compiler ](img/C14583_08_02.jpg) ###### 图 8.2: C++ 编译器 完成后,左侧窗格的代码会自动重新编译,并且会生成汇编代码并显示在右侧窗格中。这里,输出是彩色编码的,以显示哪些汇编代码行是从哪些 C++ 代码行生成的。下面的屏幕截图显示了生成的程序集代码: ![Figure 8.3: Assembly result ](img/C14583_08_03.jpg) ###### 图 8.3:装配结果 让我们简单分析一下前面的汇编代码。汇编语言中的每条指令都由一个**操作码**和一个或多个**操作数**组成,它们可以是寄存器、常量值或内存地址。一个**寄存器**是 CPU 中一个非常快的存储位置。在 x86 架构中,主要有 8 个寄存器,分别是 **RAX** 、 **RBX** 、 **RCX** 、 **RDX** 、 **RSI** 、 **RDI** 、 **RSP** 、 **RBP** 。英特尔 x86/x64 架构使用了一种奇怪的寄存器命名模式: * **RAX** 是一个通用的 64 位整数寄存器。 * **EAX** 指的是`RAX`的底部 32 位。 * **AX** 指的是`EAX`的底部 16 位。 * **AL** 和 **AH** 分别指`AX`的底部和顶部 8 位。 同样的惯例也适用于其他通用寄存器,如`RBX`、`RCX`和`RDX`。`RSI`、`RDI`和`RBP`寄存器有 16 位和 32 位版本,但没有 8 位子寄存器。指令的操作码可以有几种类型,包括算术、逻辑、按位、比较或跳转操作。通常将操作码称为指令。例如,“ **mov 指令**是指`操作码`为 **mov** 的指令。下面是我们的`sum`函数的汇编代码快照: ![Figure 8.4: Assembly code of the sum function ](img/C14583_08_04.jpg) ###### 图 8.4:求和函数的汇编代码 在前面的截图中,前几行被称为一个**函数序言**,也就是用来设置**栈帧**和局部变量的指令。堆栈框架表示包含参数和局部变量的函数中的本地化数据。当函数返回时,堆栈帧被丢弃。 **mov** 指令用常数值初始化寄存器或存储单元。这里汇编代码的语法叫做**英特尔语法**。这种语法的惯例是目标操作数总是第一个。例如,`RBX MOV RAX`装配代码表示将`RBX`寄存器中的值移动到`RAX`寄存器中。 #### 注意 汇编语言通常不区分大小写,所以`EAX`和`eax`的意思是一样的。 装配中的 **DWORD PTR [rbp-8]** 表达相当于`(*(DWORD*)(rbp - 8))` C 表达。换句话说,存储器地址 **rbp-8** 作为一个`4`字节`DWORD`被访问(存储器的双字–32 位)。汇编代码中的方括号表示取消引用,很像 C/C++ 中的*运算符。`rbp`寄存器是始终包含当前正在执行的函数栈的基址的基址指针。不一定要确切知道这个堆栈框架是如何工作的,但是请记住,由于堆栈从较高的地址开始并向下移动,函数参数和局部变量的地址是从`rbp`开始的负偏移。如果您看到与`rbp`有一些负偏移,它指的是局部变量或参数。 在前面的截图中,第一条 **mov** 指令将来自 **edi** 寄存器的值放入堆栈中——在本例中,它代表传入的`n`参数。最后两条 **mov** 指令将我们代码中的`ret`变量和`i`循环变量分别初始化为`0`和`1`。 现在,检查序言和初始化之后的汇编代码的快照——这是我们对()循环的**:** ![Figure 8.5: Assembly code of the for loop ](img/C14583_08_05.jpg) ###### 图 8.5:for 循环的汇编代码 在前面的截图中,字符串后跟冒号的行被称为**标签**。它们非常类似于编程语言中的标签,如`BASIC`、`C/C++ `或`Pascal`,用作**跳转**指令的目标(汇编语言中相当于`转到`语句)。 x86 汇编上以 J 开头的指令都是跳转指令,如 **JMP** 、 **JG** 、 **JGE** 、 **JL** 、 **JLE** 、 **JE** 、 **JNE** 等等。跳转指令是有条件或无条件的 gotos。在上一个截图中, **mov** 指令将`i`变量的值从内存加载到 **eax** 寄存器中。然后,用 **cmp** 指令将其与存储器中的`n`值进行比较。 #### 注意 这里的 **JG** 指令是指**如果大于**就跳。 如果比较较大,则执行跳转到 **.L2** 标签(在循环之外)。如果没有,则继续执行下一条指令,如下所示: ![Figure 8.6: Assembly code of the next instruction ](img/C14583_08_06.jpg) ###### 图 8.6:下一条指令的汇编代码 这里 **i** 的值再次重新加载到 **eax** 中,这似乎是不必要的。但是请记住,这个汇编代码没有优化,所以编译器生成的代码可能不是最佳的,并且可能包含不必要的工作。然后将 **eax** 中的值加到 **ret** 中,之后将 **1** 加到 **i** 中。最后,执行跳回 **.L3** 标签。 **.L2** 和 **.L3** 标签之间的这些指令形成了执行循环的**的代码,并汇总了直到 **n** 的整数序列,如下所示:** ![Figure 8.7: Assembly code of the for loop ](img/C14583_08_07.jpg) ###### 图 8.7:for 循环的汇编代码 这被称为**函数 epilog** 。首先,要返回的值`ret`被移入 **eax** 寄存器–这通常是存储函数返回值的寄存器。然后,堆叠框架复位,最后`ret`从`sum()`功能返回。 #### 注意 上面程序集列表中的“ret”是 RETURN 指令的助记符,不应该与我们的 C++ 代码示例中的“ret”变量混淆。 弄清楚一系列汇编指令的作用并不是一件简单的工作,但是通过观察以下几点可以获得源代码和指令之间映射的一般概念: * 代码中的常量值可以在程序集中直接识别。 * 诸如`相加`、`子`、`imul`、`idiv`等算术运算都可以识别。 * 条件跳转映射到循环和条件。 * 可以直接读取函数调用(函数名出现在汇编代码中)。 现在,让我们观察一下代码的效果,如果我们在右上角的编译器选项字段中添加一个编译器优化标志: ![Figure 8.8: Adding a compiler flag for optimization ](img/C14583_08_08.jpg) ###### 图 8.8:为优化添加编译器标志 在上图截图中, **O3** 代表**最大优化**。其他标志,如 **-mno-avx** 、 **-mno-sse** 、 **-mno-sse2** ,用于防止编译器生成与当前示例无关的**向量指令**。我们可以看到编译器不再访问内存,只使用寄存器。请注意 **xor eax,eax** 这一行,它具有将`0`存储在 **eax** 中的效果——这比将常量`0`从内存加载到寄存器中更有效。由于访问内存需要几个时钟周期(从`5`到`100`时钟周期),仅使用寄存器本身就会产生巨大的加速。 当下拉菜单中的编译器更改为 **x86-64 clang 8.0.0** 时,汇编代码发生了变化,如下图截图所示: ![Figure 8.9: Assembly code with the new compiler ](img/C14583_08_09.jpg) ###### 图 8.9:使用新编译器的汇编代码 在前面的装配清单中,注意没有以`J`开始的指令(用于跳转)。因此,根本没有循环构造!让我们来看看编译器是如何计算`1`到`n`的和的。如果`n`的值为`< = 0`,则跳至 **.LBB0_1** 标签退出,返回`0`。让我们分析以下说明: ![Figure 8.10: Assembly code with the new compiler ](img/C14583_08_10.jpg) ###### 图 8.10:使用新编译器的汇编代码 下面的代码相当于前面的指令。记住`n`在`EDI`寄存器中(因此也在 RDI 寄存器中,因为它们重叠): ```cpp eax = n - 1; ecx = n - 2; rcx *= rax; rcx >>= 1; eax = rcx + 2 * n; eax--; return eax; ``` 或者,如果我们将它写在一行中,它将如下所示: ```cpp return ((n-1) * (n-2) / 2) + (n * 2) - 1; ``` 如果我们简化这个表达式,我们会得到以下结果: ```cpp ((n^2 - 3n + 2) / 2) + 2n - 1 ``` 或者,我们可以用以下格式编写它: ```cpp ((n^2 - 3n + 2) + 4n - 2) / 2 ``` 这可以简化为以下内容: ```cpp (n^2 + n) / 2 ``` 或者,我们可以编写以下内容: ```cpp (n * (n+1)) / 2 ``` 这是数字`1`到`n`相加的封闭形式方程,也是最快的计算方法。编译器非常聪明——不仅仅是一行行地查看我们的代码,它推断我们的循环的效果是计算总和,并且它自己计算出代数。它没有计算出最简单的表达式,而是一个需要一些额外操作的等价表达式。然而,去掉循环使得这个函数非常优化。 如果我们为循环修改**中 **i** 变量的初始值或最终值来创建不同的求和,编译器仍然能够执行必要的代数操作来导出不需要循环的封闭形式解。** 这只是编译器如何变得极其高效并且看起来近乎智能的一个例子。然而,我们必须理解,这种特殊的求和优化已经被专门编程到`铿锵`编译器中。这并不意味着编译器可以对任何可能的循环计算进行这种技巧——这实际上需要编译器拥有一般的人工智能,以及世界上所有的数学知识。 让我们通过生成的汇编代码来探索编译器优化的另一个例子。请看下面的代码: ```cpp #include int three() {   const std::vector v = {1, 2};   return v[0] + v[1]; } ``` 在编译器选项中,如果我们选择 **x86-64 clang 8.0.0** 编译器并添加 **-O3 -stdlib=libc++** ,将生成以下汇编代码: ![Figure 8.11: Assembly code generated with the new compiler ](img/C14583_08_11.jpg) ###### 图 8.11:用新编译器生成的汇编代码 正如你在前面的截图中看到的,编译器正确地判断出向量与函数无关,并移除了所有包袱。它还在编译时做加法,并直接使用结果`3`作为常数。本节的主要内容如下: * 给定正确的选项,编译器在优化代码时会非常聪明。 * 研究生成的汇编代码对于获得执行复杂度的高级估计非常有用。 * 对机器代码如何工作的基本理解对任何 C++ 程序员来说都是有价值的。 在下一节中,我们将学习手动执行计时。 ### 手动执行计时 这是快速计时小程序最简单的方法。我们可以使用命令行工具来测量程序执行所需的时间。在 Windows 7 及更高版本上,可以使用以下 PowerShell 命令: ```cpp powershell -Command "Measure-Command {}" ``` 在`Linux`、`MacOS`和其他`类 UNIX`系统上,可以使用`时间`命令: ```cpp time ``` 在下一节中,我们将实现一个小程序,并研究一些关于程序执行时间的注意事项。 ### 练习 1:为程序的执行计时 在本练习中,我们将编写一个对数组求和的程序。这里的想法是给求和函数计时。当我们想要测试一个独立编写的函数时,这个方法很有用。因此,测试程序的唯一目的是执行一个单一的功能。由于计算非常简单,我们将需要运行函数数千次才能获得可测量的执行时间。在这种情况下,我们将从`main()`函数调用`sumVector()`函数,传递一个随机整数的`std::vector`。 #### 注意 旨在测试单个功能的程序有时被称为**驱动程序**(不要与设备驱动程序混淆)。 执行以下步骤完成本练习: 1. 创建一个名为 **Snippet1.cpp** 的文件。 2. 定义一个名为`sumVector`的函数,该函数对循环中的每个元素进行求和: ```cpp int sumVector(std::vector &v) {   int ret = 0;   for(int i: v)   {     ret += i;   }   return ret; } ``` 3. 定义`主`功能。使用 C++ 11 随机数生成工具初始化`10000`元素的向量,然后调用`sumVector`函数`1000`次。编写以下代码来实现这一点: ```cpp #include #include int main() {   // Initialize a random number generator   std::random_device dev;   std::mt19937 rng(dev());   // Create a distribution range from 0 to 1000   std::uniform_int_distribution dist(0,1000);   // Fill 10000 numbers in a vector   std::vector v;   v.reserve(10000);   for(int i = 0; i < 10000; ++ i)   {     v.push_back(dist(rng));   }   // Call out function 1000 times, accumulating to a total sum   double total = 0.0;   for(int i = 0; i < 1000; ++ i)   {     total += sumVector(v);   }   std::cout << "Total: " << total << std::endl; } ``` 4. Compile, run, and time this program on a Linux Terminal using the following commands: ```cpp $ g++ Snippet1.cpp $ time ./a.out ``` 前一个命令的输出如下: ![Figure 8.12: Output of timing the Snippet1.cpp code ](img/C14583_08_12.jpg) ###### 图 8.12:snippet 1 . CPP 代码的输出时序 从前面的输出可以看出,对于这个系统,程序在`0.122`秒内执行(注意,结果会有所不同,这取决于您的系统配置)。如果我们重复运行这个定时命令,结果可能会略有变化,因为程序将在第一次运行后加载到内存中,速度会稍微快一些。最好将程序运行并计时约`5`次,得到一个平均值。我们通常对花费时间的绝对值不感兴趣,而是对优化代码时该值如何提高感兴趣。 5. Use the following commands to explore the effect of using compiler optimization flags: ```cpp $ g++ -O3 Snippet1.cpp $ time ./a.out ``` 输出如下: ![Figure 8.13: Output of timing the Snippet1.cpp code compiled with -O3 ](img/C14583_08_13.jpg) ###### 图 8.13:用-O3 编译的 Snippet1.cpp 代码的输出时序 从前面的输出来看,程序似乎变得快了大约`60`倍,这似乎相当不可思议。 6. 更改代码以执行循环`100,000 次`而不是`1,000 次`: ```cpp // Call out function 100000 times for(int i = 0; i < 100000; ++ i) {   total += sumVector(v); } ``` 7. Recompile and time again using the following commands: ```cpp $ g++ -O3 Snippet1.cpp $ time ./a.out ``` 执行上一个命令后的输出如下: ![Figure 8.14: Output of timing the Snippet1.cpp code with 10,000 iterations ](img/C14583_08_14.jpg) ###### 图 8.14:10,000 次迭代的 Snippet1.cpp 代码定时输出 从前面的输出来看,似乎仍然需要完全相同的时间。这似乎是不可能的,但实际上发生的是,由于我们从未在程序中造成任何副作用,如打印总和,编译器可以自由地用空程序替换我们的代码。在功能上,根据 C++ 标准,这个程序和一个空程序是一样的,因为运行它没有副作用。 8. Open the Compiler Explorer and paste in the entire code. Set the compiler options to `-O3` and observe the generated code: ![Figure 8.15: Snippet1.cpp code in Compiler Explorer ](img/C14583_08_15.jpg) ###### 图 8.15:编译器资源管理器中的 Snippet1.cpp 代码 从前面的截图中可以看到,循环的**内的行没有颜色编码,也没有为它们生成汇编代码。** 9. 更改代码以确保必须通过打印一个值来执行求和,该值取决于以下行的计算: ```cpp std::cout<<"Total:"< #include #include #include #include using std::map; using std::string; using std::cerr; using std::endl; ``` 3. Define the `Timer` class and the class member functions by writing the following code: ```cpp class Timer {   static map ms_Counts;   static map ms_Times;   const string &m_sName;   std::chrono::time_point m_tmStart; ``` 从前面的代码中可以看到,类成员由一个名称、一个起始时间戳和两个`静态映射`组成。这个类的每个实例都意味着对某个代码块计时。该块可以是一个函数范围,也可以是由花括号分隔的任何其他块。使用模式是在块的顶部定义一个`定时器`类的实例,同时传入一个名称(可以是一个函数名或一些其他方便的标签)。实例化时,记录当前时间戳,当块退出时,该类的析构函数记录该块的累计运行时间,以及该块的执行次数。时间和计数分别存储在静态地图`毫秒时间`和`毫秒计数`中。 4. 通过编写以下代码来定义`定时器`类的构造函数: ```cpp public:   // When constructed, save the name and current clock time   Timer(const string &sName): m_sName(sName)   {     m_tmStart = std::chrono::high_resolution_clock::now();   } ``` 5. Define the destructor of the `Timer` class by writing the following code: ```cpp   // When destroyed, add the time elapsed and also increment the count under this name   ~Timer()   {     auto tmNow = std::chrono::high_resolution_clock::now();     auto msElapsed = std::chrono::duration_cast(tmNow - m_tmStart);     ms_Counts[m_sName]++ ;     ms_Times[m_sName] += msElapsed.count();   } ``` 在前面的代码中,经过的时间以毫秒为单位计算。然后,我们将它添加到这个块名的累计运行时间中,并增加这个块被执行的次数。 6. Define a `static` function named `dump()` that prints out the summary of the timed results: ```cpp   // Print out the stats for each measured block/function   static void dump()   {     cerr << "Name\t\t\tCount\t\t\tTime(ms)\t\tAverage(ms)\n";     cerr << "-----------------------------------------------------------------------------------\n";     for(const auto& it: ms_Times)     {       auto iCount = ms_Counts[it.first];       cerr << it.first << "\t\t\t" << iCount << "\t\t\t" << it.second << "\t\t\t" << it.second / iCount << "\n";     }   } }; ``` 在前面的代码中,名称、执行计数、总时间和平均时间以表格形式打印。我们在字段名称和字段值之间使用多个选项卡,使它们在控制台上垂直排列。这个功能可以按照我们的意愿修改。例如,我们可以修改这段代码,将输出转储为 CSV 文件,这样就可以将其导入到电子表格中进行进一步分析。 7. 最后定义`静态`成员完成类: ```cpp // Define static members map Timer::ms_Counts; map Timer::ms_Times; const int64_t N = 1'000'000'000; ``` 8. Now that we have defined the `Timer` class, define two simple functions that we will time as an example. One will add and the other will multiply. Since these operations are trivial, we will loop `1 billion times` so that we can have some measurable result. #### 注意 C++ 14 及更高版本让我们在整数常量中使用单引号符号来提高可读性;比如我们可以写`1'000'000`而不是`1000000`。 写出加法和乘法的函数。这两个函数只需分别将整数`1`和`N`相加并相乘: ```cpp unsigned int testMul() {   Timer t("Mul");   unsigned int x = 1;   for(int i = 0; i < N; ++ i)   {     x *= i;   }   return x; } unsigned int testAdd() {   Timer t("Add");   unsigned int x = 1;   for(int i = 0; i < N; ++ i)   {     x += i;   }   return x; } ``` 在前面的代码中,我们使用`无符号整数`作为变量,我们重复`加` / `乘`。我们使用了无符号类型,这样在算术运算中溢出就不会导致未定义的行为。如果我们使用签名类型,程序将有未定义的行为,并且不能保证以任何方式工作。其次,我们从`test DD()`和`testMul()`函数返回计算值,这样我们就可以确保编译器不会删除代码(因为没有副作用)。为了给这些函数计时,我们只需要在函数的开始用合适的标签声明一个`定时器`类的实例。一旦`定时器`对象被实例化,计时就开始,当该对象超出范围时计时停止。 9. Write the `main` function, where we will simply call both test functions `10` times each: ```cpp int main() {   volatile unsigned int dummy;   for(int i = 0; i < 10; ++ i)     dummy = testAdd();   for(int i = 0; i < 10; ++ i)     dummy = testMul();   Timer::dump(); } ``` 正如您在前面的代码中看到的,我们调用每个函数`10 次`,这样我们就可以演示`定时器`类对一个函数的多次运行进行计时。将函数的结果赋给易失性变量会迫使编译器假定存在全局副作用。因此,它不会省略我们测试函数中的代码。退出前,我们调用`定时器::转储`静态功能显示结果。 10. Save the program and open a terminal. Compile and run the program with different optimization levels – on the `gcc` and `clang` compilers, this is specified by the `-ON` compiler flag, where `N` is a number from `1` to `3`. Add the `-O1` compiler flag first: ```cpp $ g++ -O1 Snippet2.cpp && ./a.out ``` 此代码生成以下输出: ![Figure 8.17: Snippet2.cpp code performance when compiled with the -O1 option ](img/C14583_08_17.jpg) ###### 图 8.17:使用-O1 选项编译时的 Snippet2.cpp 代码性能 11. Now, add the `-O2` compiler flag in the terminal and execute the program: ```cpp $ g++ -O2 Snippet2.cpp && ./a.out ``` 这将生成以下输出: ![Figure 8.18: Snippet2.cpp code performance when compiled with the -O2 option ](img/C14583_08_18.jpg) ###### 图 8.18:使用-O2 选项编译时的 Snippet2.cpp 代码性能 12. Add the `-O3` compiler flag in the terminal and execute the program: ```cpp $ g++ -O3 Snippet2.cpp && ./a.out ``` 这将生成以下输出: ![Figure 8.19: Snippet2.cpp code performance when compiled with the -O3 option ](img/C14583_08_19.jpg) ###### 图 8.19:使用-O3 选项编译时的 Snippet2.cpp 代码性能 请注意,`testMul`功能仅在`O3`变得更快,但是`testdd`功能在`O2`变得更快,在`O3`变得更快。我们可以通过多次运行程序并平均时间来验证这一点。没有明显的理由说明为什么有些功能会加速,而有些则不会。我们必须彻底检查生成的代码,以了解原因。不能保证这种情况会发生在所有具有不同编译器甚至编译器版本的系统上。要记住的要点是,我们永远不能假设性能,但必须始终衡量它,如果我们认为我们所做的任何更改都会影响性能,则始终重新衡量。 13. 为了更容易地使用我们的`定时器`类来为单个函数计时,我们可以编写一个宏。C++ 11 及更高版本支持一个名为`__func__`的特殊编译器内置宏,该宏始终包含当前正在执行的函数名作为`const char*`。用它来定义一个宏,这样我们就不需要为我们的`定时器`实例指定一个标签,如下所示: ```cpp #define TIME_IT Timer t(__func__) ``` 14. 将`TIME_IT`宏添加到这两个函数的开头,更改创建定时器对象的现有行: ```cpp unsigned int testMul() {   TIME_IT; unsigned int testAdd() {   TIME_IT; ``` 15. Save the program and open the terminal. Compile and run it again by using the following command: ```cpp $ g++ -O3 Snippet2.cpp && ./a.out ``` 前一个命令的输出如下: ![Figure 8.20: Snippet2.cpp code output when using a macro for timing ](img/C14583_08_20.jpg) ###### 图 8.20:使用宏计时时的 Snippet2.cpp 代码输出 在前面的输出中,请注意现在已经打印了实际的函数名。使用这个宏的另一个优点是,我们可以在默认情况下将它添加到所有潜在的耗时函数中,并通过简单地将定义更改为 no-op 来禁用它,这将导致计时代码永远不会运行,从而避免了大量编辑代码的需要。在接下来的练习中,我们将使用相同的 Timer 类进行计时编码。 ## 运行时分析 **剖析**是一种测量程序中功能性能的非侵入式方法。剖析器的工作方式是以频繁的时间间隔(每秒数百次)对程序的当前执行地址进行采样,并记录当时正在执行的函数。这是一种统计抽样方法,具有合理的准确性。但是,有时结果可能会令人困惑,因为程序可能会在作为操作系统内核一部分的函数上花费大量时间。Linux 上最流行的运行时分析工具是 **perf** 。在下一节中,我们将使用 perf 来描述我们的程序。 ### 练习 3:使用性能分析程序 `perf`可以安装在`Ubuntu`上,如下所示: ```cpp apt-get install linux-tools-common linux-tools-generic ``` 为了熟悉使用`perf`的基础知识,我们将借助`perf`工具对上一练习中的程序进行分析。执行以下步骤完成本练习: 1. 打开我们在上一个练习中创建的 **Snippet2.cpp** 文件,从两个函数中删除`TIME_IT`宏。 2. Open the terminal, recompile the code again with the `-O3` flag, and then create a profile data sample with `perf` as follows: ```cpp $ g++ -O3 Snippet2.cpp $ perf record ./a.out ``` 前一个命令的输出如下: ![](img/C14583_08_21.jpg) ###### 图 8.21:使用 perf 命令分析 Snippet2.cpp 中的代码 这将创建一个名为`性能数据`的文件,可以对其进行分析或可视化。 3. Now, use the following command to visualize the recorded data: ```cpp $ perf report ``` 执行上一个命令后,基于控制台的图形用户界面将显示以下数据: ![Figure 8.22: Using the perf command to analyze the code in Snippet2.cpp ](img/C14583_08_22.jpg) ###### 图 8.22:使用 perf 命令分析 Snippet2.cpp 中的代码 您可以上下移动光标选择一个功能,然后按*进入*获取选项列表。 4. 突出显示`测试程序`,按*进入*,在结果列表中选择`注释测试程序`。显示了一个汇编代码列表,其中的注释描述了每行代码的执行时间百分比,如下所示: ![Figure 8.23: Viewing the timing statistics using the perf command for the Snippet2.cpp code ](img/C14583_08_23.jpg) ###### 图 8.23:使用 Snippet2.cpp 代码的 perf 命令查看时序统计 注意**整数乘法** ( **IMUL)** 指令(整数乘法)占据了`99%`的执行时间。传统上,整数乘法在`x86`架构上总是很昂贵,即使在最新一代的 CPU 中也是如此。该注释视图在每个跳转或分支指令旁边显示箭头,当高亮显示时,显示它与什么比较指令相关联,以及它跳转到什么地址的线图。您可以通过按左箭头键导航到上一个视图,并使用 *q* 键退出程序。 到目前为止,我们已经研究了几种用于评估程序性能的方法。这是优化最关键的阶段,因为它告诉我们需要将我们的努力引向何处。在接下来的章节中,我们将探索各种有助于优化代码的技术。 ## 优化策略 代码优化可以通过几种方式完成,例如: * 基于编译器的优化 * 源代码微优化 * 缓存友好代码 * 算法优化 在这里,每种技术都有其利弊。我们将在接下来的章节中详细研究这些方法。粗略地说,这些是根据所需的努力和性能的潜在提高来排序的。我们将在下一节研究基于编译器的优化。 ### 基于编译器的优化 将正确的选项传递给编译器可以带来许多性能上的好处。这方面的一个真实例子是英特尔创建的 Clear Linux **发行版** (Linux 发行版)。编译这个发行版是为了从所有代码中提取最大的性能,并且在大多数基准测试中比大多数其他 Linux 发行版的性能高出 30%,这是一个非常显著的加速。在 **gcc** 和 **clang** 系列编译器上,最基本的优化选项是**-OT7】,其中 **N** 是数字 **1** 、 **2** 或 **3** 之一。 **-O3** 几乎启用了编译器中的所有优化,但还有其他几个未被该标志启用的优化会有所不同。** ### 循环展开 **循环展开**是编译器可以用来减少执行分支数量的技术。每次执行分支时,都会有一定的性能开销。这可以通过多次重复循环体并减少循环的执行次数来减少。程序员可以在源代码级别完成循环展开,但是现代编译器自动完成了非常好的工作。 即使现代处理器通过**分支预测**和**推测执行**电路减轻了分支的开销,循环展开仍然产生性能优势。循环展开优化可以在带有`-funroll-loops`命令行标志的`gcc`和`clang`系列编译器上启用。在下一节中,我们将测试启用和未启用循环展开的程序的性能。 ### 练习 4:使用循环展开优化 在本练习中,我们将编写一个使用嵌套循环的简单程序,并在启用和不启用循环展开的情况下测试其性能。我们将理解编译器实现循环自动展开的方式。 执行以下步骤完成本练习: 1. 创建一个名为 **Snippet3.cpp** 的文件。 2. 写一个程序,取第一个`10,000 个`数字,打印出其中有多少是相互的因素(完整代码可以在 **Snippet3.cpp** 中找到): ```cpp # include int main() {   int ret = 0;   for(size_t i = 1; i < 10000; ++ i)   {     for(size_t j = 1; j < 10000; ++ j)     {       if(i % j == 0)       {         ++ ret;       }     }   }   std::cout << "Result: " << ret << std::endl; } ``` 3. Save the program and open the terminal. Compile the program with the `-O3` flag first and time it using the following command: ```cpp $ g++ -O3 Snippet3.cpp $ time ./a.out ``` 前一个命令的输出如下: ![Figure 8.24: Output of the code in Snippet3.cpp ](img/C14583_08_24.jpg) ###### 图 8.24:snippet 3 . CPP 中代码的输出 4. Now, compile the same code with the loop unrolling enabled and time it again: ```cpp $ g++ -O3 -funroll-loops Snippet3.cpp $ time ./a.out ``` 前一个命令的输出如下: ![Figure 8.25: Output of the code in Snippet3.cpp compiled with the loop unrolling option ](img/C14583_08_25.jpg) ###### 图 8.25:使用循环展开选项编译的 Snippet3.cpp 中的代码输出 5. 打开`Godbolt 编译器浏览器`,将前面完整的代码粘贴到左侧。 6. On the right-hand side, select `x86-64 gcc 8.3` from the compiler options and write the `-O3` flag in the options. Assembly code will be generated. For the for loop, you'll see the following output: ![Figure 8.26: Assembly code of the for loop ](img/C14583_08_26.jpg) ###### 图 8.26:for 循环的汇编代码 从前面的截图中,可以清楚地看到`CMP`指令将`RCX`与`10,000`进行比较,后面是条件跳转,`JNE`(不等则跳转)。就在这个代码之后,看到了外环比较,其中`RSI`被比较为`10,000`,随后是另一个条件跳转到`L4`标签。总的来说,内部条件分支和跳转执行`100,000,000`次。 7. 现在,添加以下选项:`-O3–funroll-loops`。将生成汇编代码。在这段代码中,您会注意到这个代码模式重复了八次(除了`LEA`指令,其偏移值发生了变化): ![Figure 8.27: Assembly code of the for loop ](img/C14583_08_27.jpg) ###### 图 8.27:for 循环的汇编代码 编译器决定将循环体展开八次,将条件跳转指令的执行次数减少了`87.5%`倍(约为`830 万`次)。仅这一项就使执行时间提高了`10%`,这是一个非常显著的加速。在本练习中,我们已经看到了循环展开的好处——接下来,我们将学习概要文件引导的优化。 ### 轮廓导向优化 **配置文件引导优化** (PGO)是大多数编译器都支持的功能。当在启用 PGO 的情况下编译程序时,编译器会向程序中添加插装代码。运行此支持 PGO 的可执行文件会创建一个日志文件,其中包含程序执行统计信息。术语**剖析**指的是运行程序以收集性能指标的过程。通常,此分析阶段应该使用真实数据集运行,以便生成准确的日志。在这个分析运行之后,用一个特殊的编译器标志重新编译程序。此标志使编译器能够基于记录的统计执行数据执行特殊优化。使用这种方法可以获得显著的性能提升。让我们解决一个基于轮廓引导优化的练习,以更好地理解这一点。 ### 练习 5:使用轮廓引导优化 在本练习中,我们将对上一练习中的代码使用概要文件引导优化。我们将了解如何使用`gcc`编译器进行配置文件引导优化。 执行以下步骤完成本练习: 1. 打开终端,在启用分析的情况下编译上一练习中的代码。包括我们需要的任何其他优化标志(在本例中为`-O3`)。编写以下代码来实现这一点: ```cpp $ g++ -O3 -fprofile-generate Snippet3.cpp ``` 2. Now, run the profiled version of the code by writing the following command: ```cpp $ ./a.out ``` 程序正常运行并打印结果,看不到其他输出-但是它生成一个包含数据的文件,这些数据将在下一步帮助编译器。请注意,在启用分析的情况下,程序的执行速度比正常情况下慢几倍。这是大型程序需要记住的。执行完上一条命令后,会生成一个名为`Snippet3.gcda`的文件,其中包含概要数据。在使用大型复杂应用时,使用生产环境中最常见的数据集和工作流来运行程序非常重要。通过在这里正确选择数据,最终的性能增益将会更高。 3. Recompile with the PGO optimization flags, that is, `-fprofile-use` and`-fprofile-correction`, as illustrated in the following code: ```cpp $ g++ -O3 -fprofile-use -fprofile-correction Snippet3.cpp ``` 请注意,除了与配置文件相关的编译器选项之外,其他选项必须与上一编译步骤中的选项完全相同。 4. Now, if we time the executable, we will see a large performance improvement: ```cpp $ time ./a.out ``` 前一个命令的输出如下: ![Figure 8.28: Timing results of the code in Snippet3.cpp with PGO optimization ](img/C14583_08_28.jpg) ###### 图 8.28:带有 PGO 优化的 Snippet3.cpp 中代码的计时结果 在本练习中,我们已经看到了使用编译器提供的配置文件引导优化所获得的性能优势。对于这段代码,性能的提高大约是`2.7x` -在更大的程序上,这可能会更高。 ### 并行化 如今大多数 CPU 都有多核,甚至手机也有四核处理器。我们可以非常简单地利用编译器标志来利用这种并行处理能力,编译器标志指示它生成并行代码。代码并行化的一种机制是使用 C/C++ 语言的`OpenMP`扩展。然而,这意味着要改变源代码,并详细了解如何使用这些扩展。另一个更简单的选项是`gcc`编译器特有的特性——它提供了一个扩展的标准库,实现了大多数并行运行的算法。 #### 注意 这种自动并行化只适用于 gcc 上的 STL 算法,不是 C++ 标准的一部分。C++ 17 标准为大多数算法的并行版本提出了标准库的扩展,但是还没有被所有的编译器支持。此外,为了利用这一特性,必须对代码进行大量重写。 ### 练习 6:使用编译器并行化 在本练习中,我们将使用`gcc`并行扩展功能来加速标准库函数。我们的目的是了解如何使用`gcc`并行扩展。 执行以下步骤完成本练习: 1. 创建一个名为 **Snippet4.cpp** 的文件。 2. 写一个简单的程序,用`STD::aggregate 对一个初始化的数组求和。`添加以下代码来实现: ```cpp #include #include #include #include #include #include using std::cerr; using std::endl; int main() {   // Fill 100,000,000 1s in a vector   std::vector v( 100'000'000, 1);   // Call accumulate 100 times, accumulating to a total sum   uint64_t total = 0;   for(int i = 0; i < 100; ++ i)   {     total += std::accumulate(v.begin(), v.end(), 0);   }   std::cout << "Total: " << total << std::endl; } ``` 3. Save the program and open the terminal. Compile the program normally and time the execution using the following commands: ```cpp $ g++ -O3 Snippet4.cpp $ time ./a.out ``` 前一个命令的输出如下: ![Figure 8.29: Output of the code in Snippet4.cpp ](img/C14583_08_29.jpg) ###### 图 8.29:snippet 4 . CPP 中代码的输出 4. Now, compile the code with the parallelization options, that is, `-O3 -fopenmp` and`-D_GLIBCXX_PARALLEL`: ```cpp $ g++ -O3 -fopenmp -D_GLIBCXX_PARALLEL Snippet4.cpp $ time ./a.out ``` 输出如下: ![Figure 8.30: Output of the code in Snippet4.cpp compiled with parallelization options ](img/C14583_08_30.jpg) ###### 图 8.30:使用并行化选项编译的 Snippet4.cpp 中的代码输出 在之前的输出中,`用户`字段显示累计的 CPU 时间,`真实`字段显示墙时间。两者之间的比例约为`7x`。这个比例会有所不同,取决于系统有多少个中央处理器内核(在这个特定的例子中,有八个内核)。对于这个系统,如果编译器能够执行`100%`并行化,这个比例可以达到 8 倍。请注意,即使使用了八个内核,执行时间的实际提升也只是`的 1.3x`左右。这可能是因为向量的分配和初始化占用了大部分时间。这是**阿姆达尔定律**的一个案例,它说当可并行部分被并行化时,程序的串行部分支配执行时间。尽管如此,我们在代码中还是获得了`1.3x`的加速,这是一个非常好的优化结果。 到目前为止,我们已经介绍了现代编译器中一些更有影响力的编译器优化特性。除此之外,还有其他几个优化标志,但它们可能不会对性能产生很大的改善。适用于具有许多不同源文件的大型项目的两个特定优化标志是**链接时间优化**或**链接时间代码生成**。这些都值得大型项目启用。在下一节中,我们将研究源代码微优化。 ### 源代码微优化 这些技术涉及到在源代码中使用特定的习惯用法和模式,这些习惯用法和模式通常比它们的等价物更快。在早期,这种微优化非常有成效,因为编译器不是很聪明。但是今天,编译器技术已经非常先进了,这些微优化的效果并没有那么显著。尽管如此,使用这些是一个非常好的习惯,因为即使在没有优化的情况下编译,它们也会使代码更快。即使在开发构建中,更快的代码也能节省测试和调试的时间。我们将在下一节中查看 std::vector 容器: ### 高效使用标准::矢量容器 `std::vector`是标准库中最简单、最有用的容器之一。与普通的 C 风格数组相比,它没有开销,但是有增长的能力,以及可选的边界检查。当编译时不知道元素的数量时,您几乎应该总是使用`std::vector`。 与`std::vector`一起使用的一个常见习惯用法是在循环中调用其上的`push _ back`-随着它的增长,vector 会重新分配一个新的缓冲区,该缓冲区比现有缓冲区大一定的因子(该增长因子的确切值取决于标准库的实现)。理论上,这种重新分配的成本最小,因为它很少发生,但实际上,在向量中调整大小的操作涉及将其缓冲区的元素复制到新分配的更大缓冲区,这可能非常昂贵。 我们可以使用`reserve()`方法来避免这些多次分配和复制。当我们知道一个向量将包含多少元素时,调用`reserve()`方法来预分配存储会有很大的不同。让我们在下一节中实现一个优化向量增长的练习。 ### 练习 7:优化矢量增长 在本练习中,我们将循环对`push_back`方法的效果进行计时,无论是否调用 reserve 方法。首先,我们将把前面几节中使用的`定时器`类提取到一个单独的头文件和实现文件中——这将允许我们将其用作所有后续代码片段的公共代码。执行以下步骤完成本练习: 1. 创建一个名为 **Timer.h** 的头文件。 2. 包括必要的头文件: ```cpp #include #include #include #include ``` 3. 创建一个名为`定时器`的类。在`定时器`类中,声明四个变量,即`ms_Counts`、`ms_Times`、`m_tmStart`和`m_sName`。声明一个构造函数、析构函数和`转储()`方法。添加以下代码来实现: ```cpp class Timer {   static std::map ms_Counts;   static std::map ms_Times;   std::string m_sName;   std::chrono::time_point m_tmStart;   public:     // When constructed, save the name and current clock time     Timer(std::string sName);     // When destroyed, add the time elapsed and also increment the count under this name     ~Timer();     // Print out the stats for each measured block/function     static void dump(); }; ``` 4. 通过编写以下代码,为时间函数定义一个名为`时间信息`的辅助宏: ```cpp // Helper macro to time functions #define TIME_IT Timer t(__func__) ``` 5. 一旦创建了头文件,创建一个名为 **Timer.cpp** 的新文件,并在其中包含 **Timer.h** 文件。另外,在 **Timer.cpp** 文件中编写构造函数、析构函数和`dump()`方法的实际实现。编写以下代码来实现这一点: ```cpp #include #include #include #include "Timer.h" using std::map; using std::string; using std::cerr; using std::endl; // When constructed, save the name and current clock time Timer::Timer(string sName): m_sName(sName) {   m_tmStart = std::chrono::high_resolution_clock::now(); } // When destroyed, add the time elapsed and also increment the count under this name Timer::~Timer() {   auto tmNow = std::chrono::high_resolution_clock::now();   auto msElapsed = std::chrono::duration_cast(tmNow - m_tmStart);   ms_Counts[m_sName]++ ;   ms_Times[m_sName] += msElapsed.count(); } // Print out the stats for each measured block/function void Timer::dump() {   cerr << "Name\t\t\tCount\t\t\tTime(ms)\t\tAverage(ms)\n";   cerr << "-----------------------------------------------------------------------------------\n";   for(const auto& it: ms_Times)   {     auto iCount = ms_Counts[it.first];     cerr << it.first << "\t\t\t" << iCount << "\t\t\t" << it.second << "\t\t\t" << it.second / iCount << "\n";   } } // Define static members map Timer::ms_Counts; map Timer::ms_Times; ``` 6. 现在,创建一个名为 **Snippet5.cpp** 的新文件,并使用`push_back()`方法编写两个简单地用第一个`1,000,000`整数填充向量的函数。第二个函数预先调用`reserve()`方法,但第一个函数没有。编写以下代码来实现这一点: ```cpp #include #include #include #include "Timer.h" using std::vector; using std::cerr; using std::endl; const int N = 1000000; void withoutReserve(vector &v) {   TIME_IT;   for(int i = 0; i < N; ++ i)   {     v.push_back(i);   } } void withReserve(vector &v) {   TIME_IT;   v.reserve(N);   for(int i = 0; i < N; ++ i)   {     v.push_back(i);   } } ``` 7. Now, write the `main` function. Note the use of redundant braces to ensure that the `v1` and `v2` vectors are destroyed after every iteration of the loop: ```cpp int main() {   {     vector v1;     for(int i = 0; i < 100; ++ i)     {       withoutReserve(v1);     }   }   {     vector v2;     for(int i = 0; i < 100; ++ i)     {       withReserve(v2);     }   }   Timer::dump(); } ``` 我们通过引用传递向量的原因是为了防止编译器优化两个函数中的整个代码。如果我们按值传递向量,函数将没有可见的副作用,编译器可能会完全省略函数。 8. Save the program and open the terminal. Compile the **Timer.cpp** and **Snippet5.cpp** files and run them as follows: ```cpp $ g++ -O3 Snippet5.cpp Timer.cpp $ ./a.out ``` 输出如下: ![Figure 8.31: Output of the code in Snippet5.cpp showing the effect of vector::reserve() ](img/C14583_08_31.jpg) ###### 图 8.31:snippet 5 . CPP 中代码的输出,显示了 vector::reserve()的效果 我们可以看到,调用`reserve()`的效果导致执行时间提高了 4%左右。在运行了很长时间的大型程序中,系统内存通常会变得非常碎片化。在这种情况下,通过使用`reserve()`预分配内存的改进可能会更好。一般来说,提前预留内存通常比动态增量快。即使是 Java 虚拟机,出于性能原因,在启动时也使用这种提前分配大量内存的技术。 ### 短路逻辑运算符 `&&``| |`逻辑运算符为**短路**,即: * 如果`||`运算符的左侧为`真`,则右侧不被评估。 * 如果左侧的`& &`算子为`假`,则右侧不评价。 通过将更不可能(或更便宜)的表达式保留在左侧,我们可以减少需要完成的工作量。在下一节中,我们将解决一个练习,并学习如何最佳地编写逻辑表达式。 ### 练习 8:优化逻辑运算符 在本练习中,我们将研究与逻辑运算符一起使用时对条件表达式进行排序的影响。执行以下步骤完成本练习: 1. 创建一个名为 **Snippet6.cpp** 的新文件。 2. 包括必要的库和 Timer.h 文件,我们在前面的练习中通过编写以下代码创建了该文件: ```cpp #include #include #include #include #include "Timer.h" using std::vector; using std::cerr; using std::endl; ``` 3. 定义一个名为`sum1()`的函数,计算`0`和`N`之间的整数之和。每个数字只有在满足一个或两个特定标准时才会求和。第一个条件是数量必须小于`N/2`。第二个条件是,当数除以 3 时,必须返回 2 作为余数。在这里,我们将`N`设置为`100,000,000`,这样代码就有了一些可测量的时间。编写以下代码来实现这一点: ```cpp const uint64_t N = 100000000; uint64_t sum1() {   TIME_IT;   uint64_t ret = 0;   for(uint64_t b=0; b < N; ++ b)   {     if(b % 3 == 2 || b < N/2)     {       ret += b;     }   }   return ret; } ``` 4. Now, define another function named `sum2()`. It must contain the same logic that we wrote for the previous function, `sum1()`. The only change here is that we reverse the order of the conditional expression of the `if` statement. Write the following code to implement this: ```cpp uint64_t sum2() {   TIME_IT;   uint64_t ret = 0;   for(uint64_t b=0; b < N; ++ b)   {     if(b < N/2 || b % 3 == 2)     {     ret += b;     }   }   return ret; } ``` 请注意,在`sum2`功能中,`b < N/2`条件将在一半时间内评估为真。因此,第二个条件,即`b % 3 == 2`,只对一半的迭代进行评估。为了简单起见,如果我们假设两个条件都花费 1 个时间单位,则`sum2()`花费的总时间将是`N/2 + (2 * N/2) = N * 3/2`。在`sum1()`功能的情况下,左侧的条件评估为`真的`只有 33%的时间,其余 66%的时间,两个条件都会评估。因此,估计花费的时间为`N/3 + (2 * N * 2/3) = N * 5/3`。我们预计`sum1`和`sum2`功能之间的时间比例将为`5/3`到`3/2`–即`sum1`比`慢 11%`。 5. 在主功能中增加如下代码: ```cpp int main() {   volatile uint64_t dummy = 0;   for(int i = 0; i < 100; ++ i)   {     dummy = sum1();   }   for(int i = 0; i < 100; ++ i)   {     dummy = sum2();   }   Timer::dump(); } ``` 6. Save the file and open the terminal. Compile and time the preceding program, as well as the **Timer.cpp** file, by writing the following commands: ```cpp $ g++ -O3 Snippet6.cpp Timer.cpp $ ./a.out ``` 输出如下: ![Figure 8.32: Output of the code in Snippet6.cpp showing the effect of optimizing boolean conditions ](img/C14583_08_32.jpg) ###### 图 8.32:snippet 6 . CPP 中代码的输出,显示了优化布尔条件的效果 从前面的输出可以看出,我们最终在速度上获得了大约`38%`的提升,这比预期的要多得多。为什么会这样?答案是`%`运算符执行整数除法,这比比较昂贵得多,但是编译器不会为`N/2`表达式生成除法指令,因为它是一个常数值。 `sum1()`函数代码对循环的每次迭代执行模运算,整体执行时间由除法支配。综上所述,我们必须始终考虑短路逻辑运算符,并计算表达式的每一侧是如何的,以及它执行了多少次,以便选择它们在表达式中出现的最佳顺序。这相当于做了一个概率论的期望值计算。在下一节中,我们将学习分支预测。 ### 分支预测 现代处理器使用流水线架构,这类似于工厂流水线,指令沿着流水线流动,由不同的工人同时处理。在每个时钟周期之后,指令沿着流水线移动到下一级。这意味着,尽管每条指令从开始到结束可能需要许多周期,但总吞吐量是每个周期完成一条指令。 这里的缺点是,如果有一个条件分支指令,中央处理器不知道之后要加载哪组指令(因为有两种可能的选择)。这种情况被称为**流水线停滞**,处理器必须等待直到分支的条件被完全评估,浪费了宝贵的周期。 为了缓解这种情况,现代处理器使用一种叫做**分支预测**的东西——它们试图预测分支的走向。随着分支被遇到的次数越来越多,它对分支可能采取的方式越来越有信心。 尽管如此,CPU 并不是无所不知的,所以如果它开始加载一个预测分支的指令,而后来条件分支变成了另一条路,那么分支之后的整个管道都必须被清除,实际的分支需要从头开始加载。在分支指令下游的“`装配线`”上完成的所有工作都必须丢弃,任何更改都需要反转。 这是性能的一个主要瓶颈,可以避免——最简单的方法是确保分支总是尽可能地往一个方向走——就像一个循环。 ### 练习 9:分支预测的优化 在本练习中,我们将探索和演示 CPU 分支预测对性能的影响。为了探索这一点,我们将在一个程序中编写两个函数——两个函数都使用两个嵌套循环执行相同的计算,这两个循环分别迭代`100`和`100,000,000`次。这两个函数的区别在于,在第一个函数中,外环是较大的一个,而在第二个函数中,外环是较小的一个。 对于第一个函数,外环退出时只失败一次分支预测,但是内环失败分支预测`100,000,000`次–每次退出。对于第二个,同样,外环退出时只失败了一次分支预测,而内环只失败了 100 次分支预测——每次都失败了。这些分支预测失败计数之间的因子`1,000,000`将导致第一个函数比第二个函数慢。执行以下步骤完成本练习: 1. 创建一个名为 **Snippet7.cpp** 的文件,并包含必要的库: ```cpp #include #include #include #include #include "Timer.h" using std::vector; using std::cerr; using std::endl; ``` 2. Define a function named `sum1()` with a nested loop. The outer `for` loop should cycle `N` times, whereas the inner for loop should iterate `100` times. Set the value of `N` to `100000000`. Write the following code to implement this: ```cpp const uint64_t N = 100000000; uint64_t sum1() {   TIME_IT;   uint64_t ret = 0;   for(int i = 0; i < N; ++ i)   {     for(int j = 0; j < 100; ++ j)     {       ret += i ^ j;     }   }   return ret; } ``` 如果我们假设处理器预测循环中的分支(统计上,循环末端的分支指令更有可能跳到循环的开始),那么每次 j 达到`100`–换句话说,`N`次,它就会以错误预测结束。 3. Define a new function, `sum2()`, with a nested loop. The only change here is that we must set the inner loop count to `N` and the outer loop count to `100`. Add the following code to implement this: ```cpp uint64_t sum2() {   TIME_IT;   uint64_t ret = 0;   for(int i = 0; i < 100; ++ i)   {     for(int j = 0; j < N; ++ j)     {       ret += i ^ j;     }   }   return ret; } ``` 现在,我们的推理是分支预测失误只发生`100`次。 4. 在主功能中增加如下代码: ```cpp int main() {   volatile uint64_t dummy;   dummy = sum1();   dummy = sum2();   Timer::dump(); } ``` 5. Save the file and open the terminal. Compile the preceding program, along with the **Timer.cpp** file, and time them using the following commands. Remember that you need to have the Timer.cpp and Timer.h files you created earlier in the same directory: ```cpp $ g++ -O3 Snippet7.cpp Timer.cpp $ ./a.out ``` 执行前一个命令的输出如下: ![Figure 8.33: Output of the code in Snippet7.cpp showing the effect of branch prediction optimization ](img/C14583_08_33.jpg) ###### 图 8.33:显示分支预测优化效果的 Snippet7.cpp 中的代码输出 从前面的输出可以看出,有一个小的但肯定是显著的加速,大约为`2%`,这可以归因于处理器能够更好地预测`sum2`函数的分支。在下一节中,我们将探索更多的优化技术。 ## 进一步优化 还有其他几种技术可以在您编写代码时实现;它们中的一些并不能保证产生更好的代码,但是改变你的编码习惯来反射性地做到这些,只需要很少的努力。它们不花钱,但可能会带来收益。其中一些技术如下: * 尽可能通过`常量`引用传递非基本类型的参数。即使**移动构造函数**可以使复制变得便宜,但它们仍然比使用`常量`引用涉及更多开销。 * 使用预递增(`++ i`)或预递减(`- i`)运算符,而不是后缀版本。这对于简单类型(如整数)通常没有任何用处,但是对于带有自定义增量运算符的复杂类型可能会有用处。养成写`++ i`而不是`i++ `的习惯是很好的做法,除非后增量实际上是想要的行为。除了性能优势,这样的代码通过使用正确的操作符更清楚地声明意图。 * 尽可能晚地声明变量——在 C 语言中,在函数顶部声明每个变量是很常见的,但是在 C++ 中,由于变量可以有非平凡的构造函数,因此只在使用它们的实际块中声明它们是有意义的。 * 就**循环提升**而言,如果一个循环中有任何代码或计算不随循环迭代而变化,将其移出循环是有意义的。这包括在循环体中创建对象。通常,在循环外声明它们一次会更有效。现代编译器会自动完成这项工作,但自己动手并不需要额外的努力。 * 尽可能使用`常量`。它不会改变代码的含义,但它让编译器对您的代码做出更强的假设,这可能会导致更好的优化。除此之外,使用`const`使代码更加易读和合理。 * 整数除法、模数和乘法(尤其是不是 2 的幂的数字)是 X86 硬件上最慢的操作。如果你需要在一个循环中执行这样的操作,也许你可以做一些代数操作来摆脱它们。 正如我们提到的,几个这样的优化可能是由编译器自己完成的,但是作为一种习惯,即使在调试模式下,这样做也会使代码快速,这在调试时是一个很大的优势。我们已经研究了一些用于微优化代码的技术——完成这些所需的代码变更级别相对较小,其中一些可以带来效率的重大提高。如果您想要编写更快的代码,您应该致力于随着时间的推移将这些技术集成为默认的编码风格。在下一节中,我们将了解缓存友好代码。 ## 缓存友好代码 计算机科学是在 20 世纪中期发展起来的,当时计算机几乎不存在,然而,到了 20 世纪 80 年代,大多数有用的数据结构和算法都被发现和改进了。算法复杂性分析是任何学习计算机科学的人都会遇到的一个话题——对于数据结构运算的复杂性,有很多公认的教科书定义。然而,在这些东西被分析了 50 年后,计算机已经以一种完全不同于想象的方式进化了。例如,一个常见的“事实”是列表数据结构对于插入操作来说比数组更快。这似乎是常识,因为在数组中插入一个元素需要将该元素之后的所有项移动到新的位置,而在列表中插入只是一些指针操作。我们将在下面的练习中检验这个假设。 ### 练习 10:探索缓存对数据结构的影响 在本练习中,我们将研究高速缓存对 C++ 标准库中的数组和列表的影响。执行以下步骤完成本练习: 1. 创建一个名为 **Snippet8.cpp** 的文件。 2. 包括必要的库,以及 **Timer.h** 头文件。编写以下代码来实现这一点: ```cpp #include #include #include #include #include #include #include "Timer.h" using std::vector; using std::list; using std::cerr; using std::endl; ``` 3. 创建一个常量整型变量,`N`,将其值设置为`100000` : ```cpp const int N = 100000; ``` 4. 初始化一个随机数发生器,创建一个从`0`到`1000`的分布范围。添加以下代码来实现: ```cpp std::random_device dev; std::mt19937 rng(dev()); std::uniform_int_distribution dist(0,N); ``` 5. 创建一个名为`insertRandom()`的方法,将`0`到`N`的元素随机插入容器中。添加以下代码来实现: ```cpp template void insertRandom(C &l) {   // insert one element to initialize   l.insert(l.end(), 0);   for(int i = 0; i < N; ++ i)   {     int pos = dist(rng) % l.size();     auto it = l.begin();     advance(it, pos);     l.insert(it, i);   } } ``` 6. 创建一个名为`insertStart()`的方法,并在开始时将从`0`到`N`的元素插入一个容器中。添加以下代码来实现: ```cpp template void insertStart(C &l) {   for(int i = 0; i < N; ++ i)   {     l.insert(l.begin(), i);   } } ``` 7. 创建一个名为`insertEnd()`的方法,并将从`0`到`N`的元素插入到末端的容器中。添加以下代码来实现: ```cpp template void insertEnd(C &l) {   for(int i = 0; i < N; ++ i)   {     l.insert(l.end(), i);   } } ``` 8. 在`主`法中写下以下代码: ```cpp int main() {   std::list l;   std::vector v;   // list   {     Timer t("list random");     insertRandom(l);   }   {     Timer t("list end");     insertEnd(l);       }   {     Timer t("list start");     insertStart(l);   }   // vector   {     Timer t("vect random");     insertRandom(v);   }   {     Timer t("vect end");     insertEnd(v);       }   {     Timer t("vect start");     insertStart(v);   }   cerr << endl << l.size() << endl << v.size() << endl;   Timer::dump(); } ``` 9. Save the file and open the terminal. Compile the preceding program, along with the **Timer.cpp** file, by writing the following commands: ```cpp $ g++ -O3 Snippet8.cpp Timer.cpp $ ./a.out ``` 前面的命令生成以下输出: ![Figure 8.34: Output of the code in Snippet8.cpp contrasting the timing of std::list and std::vector insertion ](img/C14583_08_34.jpg) ###### 图 8.34:snippet 8 . CPP 中代码的输出,对比了标准::列表和标准::向量插入的时间 从前面的输出可以看出,代码测量了在开始、结束和随机位置插入`标准::矢量`和`标准::列表`的`100000`整数所花费的时间。对于随机情况,向量显然以 100 倍或更大的因子获胜,甚至向量的最坏情况也比列表的随机情况快 10 倍。 为什么会这样?答案在于现代计算机架构的进化方式。CPU 时钟速度从 80 年代初的约`1 Mhz`提高到了 2019 年中期的`5 GHz`——时钟频率提高了`5000 倍`——虽然最早的 CPU 每个指令使用多个周期,但现代的 CPU 在单个内核上每个周期执行几条指令(由于流水线等先进技术,我们在前面已经介绍过)。 例如,原`英特尔 8088`上的`IDIV`指令需要超过 100 个时钟周期才能完成,而在现代处理器上,它可以在不到 5 个周期内完成。另一方面,内存带宽(读取或写入一个字节内存所需的时间)增长非常缓慢。 从历史上看,处理器的速度在 1980 年到 2010 年间提高了大约 16000 倍。与此同时,内存中的速度增加量级更小,不到 100 倍。因此,一条指令对内存的单次访问可能会导致中央处理器等待大量的时钟周期。这将是不可接受的性能下降,已经有很多技术来缓解这个问题。在我们探讨这一点之前,让我们在下一个练习中测量内存访问的影响。 ### 练习 11:测量记忆访问的影响 在本练习中,我们将研究随机访问内存对性能的影响。执行以下步骤完成本练习: 1. 创建一个名为 **Snippet9.cpp** 的新文件。 2. 包括必要的库,以及 **Timer.h** 头文件。创建两个常量整数变量`SIZE`和`N`,并将它们的值设置为`100000000`。另外,创建一个随机数生成器和一个从`0`到`N-1`的分布范围。编写以下代码来实现这一点: ```cpp #include #include #include #include #include #include #include "Timer.h" using std::vector; using std::list; using std::cerr; using std::endl; const int SIZE = 100'000'000; const int N = 100'000'000; std::random_device dev; std::mt19937 rng(dev()); std::uniform_int_distribution dist(0,SIZE-1); ``` 3. Create the `getPRIndex()` function, which returns a pseudo random index between `0` and `SIZE-1`, where `SIZE` is the number of elements in the array. Write the following code to implement this: #### 注意 稍后我们将讨论为什么使用随机数。 ```cpp uint64_t getPRIndex(uint64_t i) {   return (15485863 * i) % SIZE; } ``` 4. 编写一个名为`sum1()`的函数,随机访问一个大的数据数组,并将这些元素相加: ```cpp uint64_t sum1(vector &v) {   TIME_IT;   uint64_t sum = 0;   for(int i = 0; i < N; ++ i)   {     sum += v[getPRIndex(i)];   }   return sum; } ``` 5. 编写一个名为`sum2()`的函数,在没有任何内存访问的情况下对随机数求和: ```cpp uint64_t sum2() {   TIME_IT;   uint64_t sum = 0;   for(int i = 0; i < N; ++ i)   {     sum += getPRIndex(i);   }   return sum; } ``` 6. 在主函数中,初始化向量使得`v[i] == i`,因此`sum1()`和`sum2()`之间唯一的区别是`sum1()`访问内存而`sum2()`只执行计算。像往常一样,我们使用 volatile 来防止编译器删除所有代码,因为它没有副作用。在`主()`功能中写下以下代码: ```cpp int main() {   // Allocate SIZE integers   std::vector v(SIZE, 0);   // Fill 0 to SIZE-1 values into the vector   for(int i = 0; i < v.size(); ++ i)   {     v[i] = i;   }   volatile uint64_t asum1 = sum1(v);   volatile uint64_t asum2 = sum2();   Timer::dump(); } ``` 7. Save the program and open the terminal. Compile and run the program by writing the following commands: ```cpp $ g++ -O3 Snippet9.cpp Timer.cpp $ ./a.out ``` 上述代码生成以下输出: ![Figure 8.35: Output of the code in Snippet9.cpp contrasting the timing of computation versus random memory access ](img/C14583_08_35.jpg) ###### 图 8.35:snippet 9 . CPP 中代码的输出,对比了计算和随机存储器访问的时间 从前面的输出中,我们可以清楚地看到性能相差约`14x`的因素。 8. 创建一个名为 **Snippet10.cpp** 的新文件,并添加与 **Snippet9.cpp** 中相同的代码。新增一个名为`sum3()`的函数,线性而非随机访问内存。另外,编辑主功能。更新后的代码如下: ```cpp uint64_t sum3(vector &v) {   TIME_IT;   uint64_t sum = 0;   for(int i = 0; i < N; ++ i)   {     sum += v[i];   }   return sum; } int main() {   // Allocate SIZE integers   std::vector v(SIZE, 0);   // Fill 0 to SIZE-1 values into the vector   for(int i = 0; i < v.size(); ++ i)   {     v[i] = i;   }   volatile uint64_t asum1 = sum1(v);   volatile uint64_t asum2 = sum2();   volatile uint64_t asum3 = sum3(v);     Timer::dump(); } ``` 9. Save the file and open the Terminal. Compile and run the program: ```cpp $ g++ -O3 Snippet10.cpp Timer.cpp $ ./a.out ``` 上述命令生成以下输出: ![Figure 8.36: Output of the code in Snippet10.cpp contrasting the timing of computation versus random and linear memory access ](img/C14583_08_36.jpg) ###### 图 8.36:snippet 10 . CPP 中代码的输出,对比了计算与随机和线性内存访问的时序 在前面的输出中,请注意内存访问现在比以前快了`35`倍,`2.5`倍于`sum2()`中的计算。我们使用`sum1()`中的随机存取模式来演示线性和随机存储器存取之间的对比。是什么让线性内存访问比随机访问快得多?答案在于现代处理器中用于减轻慢速内存影响的两种机制–**缓存**和**预取**–这两种机制我们将在下面的章节中讨论。 ### 缓存 现代处理器在处理器寄存器和内存之间有多层高速缓冲存储器。这些缓存被标记为 L1、L2、L3、L4 等,其中 L1 离处理器最近,L4 离处理器最远。每个缓存层都比它下面的层更快(通常更小)。以下是`哈斯韦尔`系列处理器的缓存/内存大小和延迟示例: * L1: 32 KB,4 个周期 * L2: 256 KB,12 个周期 * L3: 6 MB,20 个周期 * L4: 128 MB,58 个周期 * 内存:许多 GB,115 个周期 缓存如何帮助提高性能的一个简单模型如下:当一个内存地址被访问时,它会在 L1 缓存中被查找——如果找到了,它会从那里被检索出来。如果没有,则在 L2 缓存中查找,如果没有找到,则在三级缓存中查找,以此类推,如果在任何缓存中都没有找到,则从内存中获取。当从内存中取出时,它存储在每个缓存中,以便以后更快地访问。这种方法本身是相当无用的,因为只有当我们一次又一次地访问相同的内存地址时,它才能提高性能。第二个方面叫做**预取**,是能够让缓存真正有回报的机制。 ### 预取 预取是这样一个过程,当执行内存访问时,附近的数据也被提取到缓存中,即使它没有被直接访问。预取的第一个方面与内存总线粒度有关——它可以被认为是“内存子系统可以发送给处理器的最小数据量是多少?”。在大多数现代处理器中,这是 64 位——换句话说,无论你从内存中请求一个字节还是一个 64 位的值,包含该地址的 64 位的整个`机器字`都是从内存中读取的。这些数据存储在每层缓存中,以便以后更快地访问。显然,这将立即提高内存性能——假设我们在地址`0x1000`读取一个字节的内存;我们还将该地址之后的其他 7 个字节放入缓存。如果我们访问地址为`0x1001`的字节,它来自缓存,避免了昂贵的内存访问。 预取的第二个方面更进一步——当一个地址的内存内容被读取时,处理器不仅读取那个内存字,而且读取更多。在 x86 系列处理器上,这是 32 到 128 个字节。这被称为**高速缓存行**大小——处理器总是以该大小的块读写内存。当中央处理器硬件检测到内存正在以线性方式被访问时,它会根据对后续可能被访问的地址的预测,将内存预取到一个高速缓存行中。 CPU 在检测规则访问模式(向前和向后)方面非常聪明,并且会高效预取。您还可以使用特殊指令向处理器提供提示,使其根据程序员的指示预取数据。这些指令在大多数编译器上作为内部函数提供,以避免使用内联汇编语言。当不在高速缓存中的存储器地址被读取或写入时,它被称为**高速缓存未命中**,并且是非常昂贵的事件,要不惜一切代价避免。中央处理器硬件尽最大努力减少缓存未命中,但程序员可以分析和修改数据访问模式,以最大限度地减少缓存未命中。这里对高速缓存的描述是一个简化的教学模型——实际上,中央处理器有指令和数据的 L1 高速缓存、多条高速缓存线和非常复杂的机制,以确保多个处理器能够保持它们各自的高速缓存同步。 #### 注意 在这篇著名的在线文章中可以找到对缓存实现的全面描述(以及关于内存子系统的许多其他信息):[https://lwn.net/Articles/250967/](https://lwn.net/Articles/250967/)。 ### 缓存对算法的影响 了解了缓存之后,我们现在可以推断为什么我们的第一个向量对比列表示例会显示出令人惊讶的结果——从计算机科学的角度来看,以下是正确的: **列表**: * 迭代到第 N 个位置是 N 阶复杂度。 * 插入或删除一个元素的复杂度是 1。 **对于数组(或向量)**: * 迭代到第 n 个位置是 1 阶复杂度。 * 在位置 N 插入或删除元素的复杂度与(S - N)成正比,其中 S 是数组的大小。 然而,对于现代体系结构,存储器访问的成本非常高,但是随后访问相邻地址的成本几乎为 0,因为它已经在高速缓存中。这意味着对非顺序位于内存中的`std::list`中的元素的迭代很可能总是导致缓存未命中,从而导致性能下降。另一方面,由于数组或`std::vector`的元素总是相邻的,缓存和预取可以大幅降低将(S-N)元素复制到新位置的总成本。因此,对这两种数据结构的传统分析宣称列表更适合随机插入,虽然在技术上是正确的,但实际上并不正确,尤其是考虑到现代 CPU 硬件明显复杂的缓存行为。当我们的程序是*数据绑定*时,算法复杂性的分析必须通过理解所谓的**数据局部性**来增加。 数据局部性可以简单地定义为从刚被访问的内存地址到先前被访问的内存地址的平均距离。换句话说,跨越彼此相距较远的地址进行内存访问是一种严重的减速,因为来自较近地址的数据很可能已经被预取到缓存中。当数据已经存在于缓存中时,称为“热”;否则,它被称为“冷”。利用缓存的代码称为**缓存友好**。另一方面,高速缓存不友好的代码会导致高速缓存行被浪费地重新加载(称为**高速缓存无效**)。在本节的剩余部分,我们将研究关于如何编写缓存友好代码的策略。 ### 优化缓存友好性 在过去,代码优化包括试图最小化代码中的机器指令数量,使用更有效的指令,甚至重新排序指令以允许流水线保持满。在当今时代,编译器执行所有上述优化的程度是大多数程序员无法做到的——尤其是考虑到编译器可以在数亿条指令的整个程序中做到这一点。即使是现在,程序员的责任仍然是优化数据访问模式以利用缓存的能力。 任务非常简单——确保访问的内存接近之前访问的内存——但是实现这一点的方法需要大量的努力。 #### 注意 90 年代著名的游戏程序员和代码优化大师 Terje Mathisen,据说说过:“所有的编程都是缓存方面的练习。”2019 年的今天,这种说法比以往任何时候都更适用于这个试图编写快速代码的子领域。 提高缓存友好性有一些基本的经验法则: * 堆栈总是“热”的,所以我们应该尽可能多地使用局部变量。 * 动态分配的对象很少彼此具有数据局部性——避免它们或者使用预先分配的对象池,以便它们在内存中是连续的。 * 基于指针的数据结构,如树,尤其是列表,由堆上分配的多个节点组成,对缓存非常不友好。 * 面向对象代码中虚拟函数的运行时分派会使指令缓存失效——避免性能关键代码中的动态分派。 在下一节中,我们将探讨堆分配的成本。 ### 练习 12:探索堆分配的成本 在本练习中,我们将研究动态分配内存对性能的影响,并研究堆内存如何影响代码的性能。执行以下步骤完成本练习: 1. 创建一个名为 **Snippet11.cpp** 的文件。 2. 添加以下代码以包含必要的库: ```cpp #include #include #include #include "Timer.h" using std::string; using std::cerr; using std::endl; ``` 3. 声明一个常量变量 N 和一个名为水果的字符数组。给它们赋值: ```cpp const int N = 10'000'000; const char* fruits[] =   {"apple", "banana", "cherry", "durian", "guava", "jackfruit", "kumquat", "mango", "orange", "pear"}; ``` 4. 创建一个名为`fun1()`的函数,该函数只循环遍历水果中的每个字符串,将其复制到一个字符串中,并对该字符串的字符进行求和: ```cpp uint64_t fun1() {   TIME_IT;   uint64_t sum = 0;   string s1;   for(uint64_t i = 0; i < N; ++ i)   {     s1 = fruits[i % 10];     for(int k = 0; k < s1.size(); ++ k) sum += s1[k];   }   return sum; } ``` 5. 创建另一个名为`sum2()`的函数,使用本地声明的字符数组代替字符串和循环来复制: ```cpp uint64_t fun2() {   TIME_IT;   uint64_t sum = 0;   char s1[32];   for(uint64_t i = 0; i < N; ++ i)   {     char *ps1 = s1;     const char *p1 = fruits[i % 10];     do { *ps1++ = *p1; } while(*p1++);     for(ps1 = s1; *ps1; ++ ps1) sum += *ps1;   }   return sum; } ``` 6. 在`main()`函数内写下以下代码: ```cpp int main() {   for(int i = 0; i < 10; ++ i)   {     volatile uint64_t asum1 = fun1();     volatile uint64_t asum2 = fun2();     }   Timer::dump(); } ``` 7. Save the file and open the terminal. Compile and run the program: ```cpp $ g++ -O3 Snippet11.cpp Timer.cpp $ ./a.out ``` 上述命令生成以下输出: ![Figure 8.37: Output of the code in Snippet11.cpp showing the effect of heap allocation on the timing ](img/C14583_08_37.jpg) ###### 图 8.37:snippet 11 . CPP 中代码的输出,显示了堆分配对计时的影响 从前面的输出中,请注意`fun2()`的速度几乎是`fun1()`的两倍。 8. Now, use the `perf` command to profile: ```cpp $ perf record ./a.out ``` 前面的命令生成以下输出: ![Figure 8.38: Output of the perf command profiling the code in Snippet11.cpp ](img/C14583_08_38.jpg) ###### 图 8.38:在 Snippet11.cpp 中分析代码的 perf 命令的输出 9. 现在,我们可以用以下代码检查性能报告: ```cpp $ perf report ``` 我们收到以下输出: ![](img/C14583_08_39.jpg) ###### 图 8.39:snippet11 . CPP 中代码的 perf 命令定时报告的输出 在前面的输出中,请注意大约 33%的执行时间是由`std::string`构造函数、`strlen()`和`memmove()`占用的。所有这些都与在`fun1()`中使用的`std::string`相关联。尤其是堆分配是最慢的操作。 ### 数组模式的结构 在许多程序中,我们经常使用相同类型的对象数组——这些对象可以代表数据库中的记录、游戏中的实体等等。一种常见的模式是遍历一个大的结构数组,并对某些字段执行操作。即使结构在内存中是顺序的,如果我们只访问几个字段,更大的结构会降低缓存的效率。 处理器可以将几个结构预取到高速缓存中,但是程序只访问高速缓存数据的一部分。因为它没有使用每个结构的每个字段,所以大部分缓存数据都会被丢弃。为了避免这种情况,可以使用另一种数据布局——我们使用一种阵列结构 ( **SoA** )模式,而不是使用结构 ( **AoS** )模式的**结构。在下一节中,我们将解决一个练习,其中我们将检查使用 SoA 模式和 AoS 模式的性能优势。** ### 练习 13:使用数组模式的结构 在本练习中,我们将研究使用 SoA 和 AoS 模式的性能优势。执行以下步骤完成本练习: 1. 创建一个名为 **Snippet12.cpp** 的文件。 2. 包括必要的库,以及`Timer.h`头文件。初始化一个随机数生成器,并创建一个从 1 到 N-1 的分布范围。创建一个常量整型变量,N,并用值 100,000,000 初始化它。添加以下代码来实现: ```cpp #include #include #include #include #include #include #include "Timer.h" using std::vector; using std::list; using std::cerr; using std::endl; const int N = 100'000'000; std::random_device dev; std::mt19937 rng(dev()); std::uniform_int_distribution dist(1,N-1); ``` 3. 编写两种不同的方式来表示数据--一种结构数组和一种数组结构。使用`uint64_t`的六个字段,这样我们就可以模拟一个更能代表真实世界程序的大尺寸结构: ```cpp struct Data1 {   uint64_t field1;   uint64_t field2;   uint64_t field3;   uint64_t field4;   uint64_t field5;   uint64_t field6; }; struct Data2 {   vector field1;   vector field2;   vector field3;   vector field4;   vector field5;   vector field6; }; struct Sum {   uint64_t field1;   uint64_t field2;   uint64_t field3;   Sum(): field1(), field2(), field3() {} }; ``` 4. 定义两个函数,即`sumAOS`和`sumSOA`,对前面两个数据结构的`字段 1`、`字段 2`和`字段 3`中的值求和。编写以下代码来实现这一点: ```cpp Sum sumAOS(vector &aos) {   TIME_IT;   Sum ret;   for(int i = 0; i < N; ++ i)   {     ret.field1 += aos[i].field1;     ret.field2 += aos[i].field2;     ret.field3 += aos[i].field3;   }   return ret; } Sum sumSOA(Data2 &soa) {   TIME_IT;   Sum ret;   for(int i = 0; i < N; ++ i)   {     ret.field1 += soa.field1[i];     ret.field2 += soa.field2[i];     ret.field3 += soa.field3[i];   }   return ret; } ``` 5. 在`主`功能中编写以下代码: ```cpp int main() {    vector arrOfStruct;    Data2 structOfArr;    // Reserve space    structOfArr.field1.reserve(N);    structOfArr.field2.reserve(N);    structOfArr.field3.reserve(N);    arrOfStruct.reserve(N);    // Fill random values    for(int i = 0; i < N; ++ i)    {      Data1 temp;      temp.field1 = dist(rng);      temp.field2  = dist(rng);      temp.field3 = dist(rng);      arrOfStruct.push_back(temp);      structOfArr.field1.push_back(temp.field1);      structOfArr.field2.push_back(temp.field2);      structOfArr.field3.push_back(temp.field3);    }   Sum s1 = sumAOS(arrOfStruct);   Sum s2 = sumSOA(structOfArr);   Timer::dump(); } ``` 6. Save the program and open the Terminal. Run the program to time it by adding the following commands: ```cpp $ g++ -O3 Snippet12.cpp Timer.cpp $ ./a.out ``` 上述代码生成以下输出: ![Figure 8.40: Output of the code in Snippet12.cpp contrasting the timing of the AOS and SOA patterns ](img/C14583_08_40.jpg) ###### 图 8.40:snippet 12 . CPP 中代码的输出,对比了 AOS 模式和 SOA 模式的时间 数组结构方法的速度是数组结构方法的两倍。考虑到结构中向量的地址会相距很远,我们可能想知道为什么在 SoA 的情况下缓存行为更好。这是因为高速缓存是如何设计的,而不是像我们前面讨论的那样,将一个高速缓存视为一个单片块,而是将其分成多行。当访问存储器地址时,32 位或 64 位地址被转换成几个位的“标签”,并且使用与该标签相关联的高速缓存行。非常接近的内存地址将获得相同的标签并到达相同的缓存行。如果访问高度不同的地址,它会到达不同的高速缓存行。这种基于行的高速缓存设计对我们测试程序的影响是,好像我们对每个向量都有独立的高速缓存。 前面对高速缓存行的解释非常简单,但高速缓存行的基本概念适用。对于这种数组模式的结构来说,代码可读性可能会稍差一些,但是考虑到性能的提高,这是非常值得的。随着结构尺寸的增大,这种特殊的优化变得更加有效。此外,请记住,如果字段大小不一,填充结构会使其大小膨胀很大一部分。我们已经探索了内存延迟对性能的影响,并了解了一些帮助处理器缓存有效的方法。当编写一个对性能至关重要的程序时,我们应该记住缓存效果。有时候,首先从一个对缓存更友好的架构开始是有意义的。像往常一样,我们应该在尝试对数据结构进行彻底的改变之前,一直衡量代码的性能。优化应该集中在程序最耗时的部分,而不是它的每一部分。 ### 算法优化 算法优化的最简单形式是寻找执行你的任务的库——最流行的库都是高度优化和编写良好的。比如`Boost`库提供了很多有用的库,可以在很多项目中派上用场,比如`Boost。几何`、`助推。图`、`升压。间隔`和`升压。多精度`,举几个例子。使用专业编写的库比自己创建库要容易和明智得多。例如`Boost。图`实现了十几个算法来处理拓扑图,每一个算法都是高度优化的。 许多计算可以简化为一系列标准算法组合在一起——如果做得正确,这些算法可以产生极其高效的代码——甚至经常被编译器并行化以利用多核或 SIMD。在本节的其余部分,我们将采用一个单独的程序,并尝试以各种方式对其进行优化–这将是一个字数统计程序,具有以下规格: * 为了隔离磁盘输入/输出所花费的时间,我们将在处理之前将整个文件读取到内存中。 * Unicode 支持将被忽略,我们将假设英语文本为 ASCII。 * 我们将使用在线提供的大量公共领域文学文本作为测试数据。 ### 练习 14:优化字数统计程序 在这个冗长的练习中,我们将使用各种优化技术来优化程序。我们将执行实际程序的增量优化。我们将使用的测试数据由名为《双城记》的书组成,该书已被附加在一起 512 次。 #### 注意 本练习中使用的数据集可在此处获得:[https://github . com/trainingypbackt/Advanced-CPlusPlus/blob/master/lesson 8/练习 14/data.7z](https://github.com/TrainingByPackt/Advanced-CPlusPlus/blob/master/Lesson8/Exercise14/data.7z) 。您将需要提取这个 7zip 存档,并将结果文件(称为 data.txt)复制到您进行本练习的文件夹中。 执行以下步骤完成本练习: 1. Write the basic boilerplate code that reads the file (the full code can be found in **SnippetWC.cpp**). This will be a common driver for all the versions of the word count that we write: ```cpp int wordCount(const string &s); int main(int argc, char **argv) {   if(argc > 1)   {     TIME_IT;     string sContent;     ostringstream buf;     ifstream ifs(argv[1]);     {       Timer t("Read file");       buf << ifs.rdbuf();       sContent = buf.str();       sContent.push_back(' ');     }     cerr << wordCount(sContent) << endl;   }   Timer::dump(); } ``` 我们将使用一个虚拟块来分隔读取文件的代码的时间,以及时间`main()`本身,以获得整体执行时间。 请注意`push_back`在末尾添加了一个空格——这确保了数据以空格结尾,简化了我们使用的算法。 2. 写一个基本的字数统计功能。逻辑非常简单——对于字符串中的每个字符,如果该字符不是空白,而后面的字符是空白,那么它就是单词的结尾,应该被计数。由于我们的样板代码在末尾增加了一个空格,所以任何最终的单词都将被计算在内。该功能在 **Snippet13.cpp** : ```cpp int wordCount(const std::string &s) {   int count = 0;   for(int i = 0, j = 1; i < s.size() - 1; ++ i, ++ j)   {     if(!isspace(s[i]) && isspace(s[j]))     {       ++ count;     }   }   return count; } ``` 中定义 3. Let's compile, run, and get an idea of the performance. We will verify that it is working right by comparing the result of our code with the results provided by the standard `wc` program: ```cpp $ g++ -O3 Snippet13.cpp SnippetWC.cpp Timer.cpp $ ./a.out data.txt ``` 我们收到以下输出: ![Figure 8.41: Output of the code in Snippet13.cpp with a baseline wordcount implementation ](img/C14583_08_41.jpg) ###### 图 8.41:带有基线字数实现的 Snippet13.cpp 中的代码输出 让我们为厕所计划计时: ```cpp $ time wc -w data.txt ``` 我们收到以下输出: ![Figure 8.42: Output of timing the wc program ](img/C14583_08_42.jpg) ###### 图 8.42:定时 wc 程序的输出 *wc* 程序显示相同的字数,即`71108096`,所以我们知道我们的代码是正确的。我们的代码花了大约`3.6 秒`,包括读取文件,比 wc 慢很多。 4. Our first strategy to optimize is to see if there is a better way to implement `isspace()`. Instead of a function, we can use a lookup table that can tell if a character is a space or not (the code for this can be found in **Snippet14.cpp**): ```cpp int wordCount(const std::string &s) {   // Create a lookup table   bool isSpace[256];   for(int i = 0; i < 256; ++ i)   {     isSpace[i] = isspace((unsigned char)i);   }   int count = 0;   int len = s.size() - 1;   for(int i = 0, j = 1; i < len; ++ i, ++ j)   {     count += !isSpace[s[i]] & isSpace[s[j]];   }   return count; } ``` 请记住,C/C++ 中的布尔变量采用整数值 0 或 1,因此我们可以直接编写以下内容: ```cpp !isSpace[s[i]] & isSpace[s[j]] ``` 这意味着我们不必写这个: ```cpp (!isSpace[s[i]] && isSpace[s[j]]) ? 1 : 0 ``` 将布尔值直接用作数字有时会导致更快的代码,因为我们避免了条件逻辑运算符&&和||,这可能会导致分支指令。 5. Compile and test the performance now: ```cpp $ g++ -O3 Snippet14.cpp SnippetWC.cpp Timer.cpp $ ./a.out data.txt ``` 我们收到以下输出: ![Figure 8.43: Output of the code in Snippet14.cpp ](img/C14583_08_43.jpg) ###### 图 8.43:snippet 14 . CPP 中代码的输出 通过使用查找表的简单原理,我们实现了字数统计代码的 8 倍加速。我们能做得更好吗?是的–我们可以进一步理解查找表的概念–对于每对字符,有四种可能,这将导致相应的操作: 【空间空间】:无动作,【非空间空间】:加 1 计数,【空间非空间】:无动作,【非空间,非空间】:无动作 因此,我们可以制作一个包含`65536`条目(`256 * 256`)的表格来覆盖所有可能的字符对。 6. Write the following code to create the table: ```cpp // Create a lookup table for every pair of chars bool table[65536]; for(int i = 0; i < 256; ++ i) {   for(int j = 0; j < 256; ++ j)   {     int idx = j + i * 256;     table[idx] = !isspace(j) && isspace(i);   } } ``` 字数循环如下(完整代码可在 **Snippet15.cpp** 中找到): ```cpp int count = 0; for(int i = 0; i < s.size() - 1; ++ i) {   // grab the 2 bytes at s[i] as a 16 bit value   unsigned short idx;   memcpy(&idx, &s[i], 2);   count += table[idx]; } ``` 我们将字符串的每个字符作为一个 16 位的值来读取。直接将一个指针从 char*强制转换为另一个类型并取消对它的引用是未定义的行为——正确的方法是使用`memcpy()`。编译器足够聪明,可以使用 CPU 内存访问指令,而不是实际调用`memcpy()`获取 2 个字节。我们已经结束了不包含条件语句的循环,这应该会使它更快。请记住,X86 架构是*小端*-因此从字符数组中读取的 16 位值将第一个字符作为其 LSB,第二个字符作为 MSB。 7. Now, time the code we wrote: ```cpp $ g++ -O3 Snippet15.cpp SnippetWC.cpp Timer.cpp $ ./a.out data.txt ``` ![Figure 8.44: Output of the code in Snippet15.cpp ](img/C14583_08_44.jpg) ###### 图 8.44:snippet 15 . CPP 中代码的输出 这个更大的查找表使`字数()`的速度提高了 1.8 倍。让我们退后一步,从另一个角度来看这个问题,这样我们就可以有效地使用现有的标准库。这样做的好处有两个方面——首先,代码不太容易出错,其次,我们可以利用一些编译器提供的并行化。 让我们使用标准算法重写使用`isspace`的查找表的程序版本。如果我们看一下计数单词的主循环,我们取 2 个字符,根据一些逻辑,我们将 1 或 0 累加到`计数`变量中。这是许多代码中常见的模式: ```cpp X OP (a[0] OP2 b[0]) OP (a[1] OP2 b[1]) OP (a[2] OP2 b[2]) ... OP (a[N] OP2 b[N])   ``` 这里`a`和`b`是大小的数组`N`,`X`是初始值,`OP`和`OP2`是运算符。有一种标准算法封装了这种模式,称为`STD::inner _ product`–它采用两个序列,在每对元素之间应用一个运算符(OP2),并在这些元素之间应用另一个运算符(OP),从初始值 x 开始。 8. We can write the function as follows (the full code can be found in **Snippet16.cpp**): ```cpp int wordCount(const std::string &s) {   // Create a lookup table for every char   bool table[256];   for(int i = 0; i < 256; ++ i)   {     table[i] = isspace((unsigned char)i) ? 1 : 0;   }   auto isWordEnd = [&](char a, char b)   {     return !table[a] & table[b];   };   return std::inner_product(s.begin(), s.end()-1, s.begin()+1, 0, std::plus(), isWordEnd); } ``` 这个`内积()`调用在每个`s[n]`和`s[n+1]`上应用`isWordEnd()` lambda,并在这些结果之间应用标准加法函数。实际上,当`s[n]`和`s[n+1]`在一个词尾时,我们是在加 1。 #### 注意 即使这看起来像是许多嵌套的函数调用,编译器也会内联所有内容,并且没有开销。 9. Compile and time the execution of this version: ```cpp $ g++ -O3 Snippet16.cpp SnippetWC.cpp Timer.cpp $ ./a.out data.txt ``` 我们收到以下输出: ![Figure 8.45: Output of the code in Snippet16.cpp ](img/C14583_08_45.jpg) ###### 图 8.45:snippet 16 . CPP 中代码的输出 令人惊讶的是,该代码比我们在 **Snippet14.cpp** 中的初始循环版本稍快。 10. Can we adapt the same code to use the large lookup table? Indeed, we can – the new function looks like this (the full code can be found in **Snippet17.cpp**): ```cpp int wordCount(const std::string &s) {   // Create a lookup table for every pair of chars   bool table[65536];   for(int i = 0; i < 256; ++ i)   {     for(int j = 0; j < 256; ++ j)     {       int idx = j + i * 256;       table[idx] = !isspace(j) && isspace(i);     }   }   auto isWordEnd = [&](char a, char b)   {     unsigned idx = (unsigned)a | (((unsigned)b) << 8);     return table[idx];   };   return std::inner_product(s.begin(), s.end()-1, s.begin()+1, 0, std::plus(), isWordEnd); } ``` 与之前基于循环的代码唯一不同的是,我们没有使用`memcpy()`将两个连续的字节转换为一个单词,而是使用按位`或`运算符将它们组合在一起。 11. Compile and time the code: ```cpp $ g++ -O3 Snippet17.cpp SnippetWC.cpp Timer.cpp $ ./a.out data.txt ``` 我们收到以下输出: ![Figure 8.46: Output of the code in Snippet17.cpp ](img/C14583_08_46.jpg) ###### 图 8.46:snippet 17 . CPP 中代码的输出 这段代码没有我们在 **Snippet15.cpp** 中的基于 loop 0 的版本快。这样做的原因是,在循环版本中,我们读取 2 个字节组合成一个`短的`来获得索引,这不需要计算,但是在这里,我们通过按位运算将 2 个字节读取到一个`短的`中。 12. Now that we have the code where the bulk of the work is done by a standard library function, we can now get automatic parallelization for free – compile and test as follows: ```cpp $ g++ -O3 -fopenmp -D_GLIBCXX_PARALLEL Snippet17.cpp SnippetWC.cpp Timer.cpp $ ./a.out data.txt ``` 我们收到以下输出: ![Figure 8.47: Output of the code in Snippet17.cpp with the parallelized standard library ](img/C14583_08_47.jpg) ###### 图 8.47:带有并行化标准库的 Snippet17.cpp 中的代码输出 显然,它不能完全并行化,所以我们在速度方面只获得了大约 2.5 倍的改进,但我们不必对代码做任何事情就能实现。我们能以同样的方式使基于循环的代码并行化吗?理论上,是的——我们可以手动使用 **OpenMP** 指令来实现这一点;然而,这需要对代码进行修改,并了解如何使用 OpenMP。 **Snippet16.cpp** 中的版本怎么样? ```cpp $ g++ -O3 -fopenmp -D_GLIBCXX_PARALLEL Snippet16.cpp SnippetWC.cpp Timer.cpp $ ./a.out data.txt ``` 我们收到以下输出: ![Figure 8.48: Output of the code in Snippet16.cpp with the parallelized standard library ](img/C14583_08_48.jpg) ###### 图 8.48:带有并行化标准库的 Snippet16.cpp 中的代码输出 这个版本也有类似的改进。我们完成了吗,或者还能更快吗?著名游戏程序员迈克尔·阿布拉什(Michael Abrash)创造了首字母缩略词“T2”——它代表“没有最快的代码”。他的意思是,只要付出足够的努力,总有可能让代码变得更快。这似乎是不可能的,但人们一次又一次地找到了越来越快的计算方法——我们的代码也不例外,我们仍然可以走得更远。我们可以为优化做的权衡之一是让代码不那么通用——我们已经对代码进行了一些限制——例如,我们只处理 **ASCII** 英文文本。通过对输入数据添加更多的约束,我们可以做得更好。让我们假设文件中没有不可打印的字符。这是对我们输入数据的合理假设。如果我们假设这一点,那么我们可以简化检测空格的条件——因为所有空格字符都大于或等于 ASCII 32,所以我们可以避免查找表本身。 13. 让我们根据之前的想法实现代码(完整代码可以在 **Snippet18.cpp** 中找到): ```cpp int wordCount(const std::string &s) {   auto isWordEnd = [&](char a, char b)   {     return a > 32 & b < 33;   };   return std::inner_product(s.begin(), s.end()-1, s.begin()+1, 0, std::plus(), isWordEnd); } ``` 14. Compile and run the program: ```cpp $ g++ -O3 Snippet18.cpp SnippetWC.cpp Timer.cpp $ ./a.out data.txt ``` 我们收到以下输出: ![Figure 8.49: Output of the code in Snippet18.cpp with simplified logic for detecting spaces ](img/C14583_08_49.jpg) ###### 图 8.49:snippet 18 . CPP 中代码的输出,带有检测空格的简化逻辑 这个版本的速度是并行版本的两倍,而且只有几行代码。使用并行化会更好吗? ```cpp $ g++ -O3 -fopenmp -D_GLIBCXX_PARALLEL Snippet18.cpp SnippetWC.cpp Timer.cpp $ ./a.out data.txt ``` 我们收到以下输出: ![Figure 8.50: Output of the code in Snippet18.cpp with the parallelized standard library ](img/C14583_08_50.jpg) ###### 图 8.50:带有并行化标准库的 Snippet18.cpp 中的代码输出 不幸的是,事实并非如此——它实际上更慢。管理多线程和线程争用的开销有时比多线程代码的好处更昂贵。在这一点上,我们可以看到文件读取代码占用了大部分时间——我们能对此做些什么吗? 15. 让我们将`main()`功能更改为对其各个部分计时(完整代码可在 **SnippetWC2.cpp** 中找到): ```cpp     {       Timer t("File read");       buf << ifs.rdbuf();     }     {       Timer t("String copy");       sContent = buf.str();     }     {       Timer t("String push");       sContent.push_back(' ');     }     int wc;     {       Timer t("Word count");       wc = wordCount(sContent);     } ``` 16. Compile and run the preceding code: ```cpp $ g++ -O3 Snippet18.cpp SnippetWC2.cpp Timer.cpp $ ./a.out data.txt ``` 我们收到以下输出: ![Figure 8.51: Output of the code in Snippet18.cpp with all operations timed ](img/C14583_08_51.jpg) ###### 图 8.51:snippet 18 . CPP 中所有操作都定时的代码输出 大部分时间由`push_back()`和复制字符串占用。由于字符串正好是文件的大小,`push_back()`最终会为字符串分配一个新的缓冲区并复制内容。如何才能消除这个`push_back()`的称呼?我们在末尾添加了一个空格,以便能够始终如一地计算最后一个单词(如果有的话),因为我们的算法计算单词的末尾。有三种方法可以避免这种情况:计算一个单词的开头,而不是结尾;单独计算最后一个词,如果有的话;并且使用`c_str()`函数,这样我们在末尾就有了一个`NUL`字符。现在让我们依次尝试这些方法。 17. 先写主功能不用`push_back`(完整代码可以在 **SnippetWC3.cpp** 找到): ```cpp {   Timer t("File read");   buf << ifs.rdbuf(); } {   Timer t("String copy");   sContent = buf.str(); } int wc; {   Timer t("Word count");   wc = wordCount(sContent); } ``` 18. 通过将`isWordEnd()`重命名为`isWordStart()`来更改 wordCount()中的代码,并反转逻辑。如果当前字符是空格,而后续字符不是空格,那么可以将一个单词视为起始字符。此外,如果字符串以非空格开头,则多计数一个单词(完整代码可在 **Snippet19.cpp** 中找到): ```cpp int wordCount(const std::string &s) {   auto isWordStart = [&](char a, char b)   {     return a < 33 & b > 32;   };   // Count the first word if any   int count = s[0] > 32;   // count the remaining   return std::inner_product(s.begin(), s.end()-1, s.begin()+1, count, std::plus(), isWordStart); } ``` 19. 现在,写下第二个选择——数数最后一个单词,如果有的话。代码与 **Snippet18.cpp** 版本几乎相同,只是我们检查了最后一个字(完整代码可以在 **Snippet20.cpp** 中找到): ```cpp int count = std::inner_product(s.begin(), s.end()-1, s.begin()+1, 0, std::plus(), isWordEnd); // count the last word if any if(s.back() > 32) {   ++ count; } return count; ``` 20. Write the third version that uses `c_str()` – all we need to do is change the parameters for `inner_product()` (the full code can be found in **Snippet21.cpp**) ```cpp int wordCount(const std::string &s) {   auto isWordEnd = [&](char a, char b)   {     return a > 32 & b < 33;   };   const char *p = s.c_str();   return std::inner_product(p, p + s.size(), p+1, 0, std::plus(), isWordEnd); } ``` 由于`c_str()`末端有一个`NUL`,所以它的工作原理和以前一样。 21. Compile and time all three versions: ```cpp $ g++ -O3 Snippet19.cpp SnippetWC3.cpp Timer.cpp $ ./a.out data.txt ``` 我们收到以下输出: ![Figure 8.52: Output of the code in Snippet19.cpp, which counts the beginnings of words rather than the ends ](img/C14583_08_52.jpg) ###### 图 8.52:snippet 19 . CPP 中代码的输出,它计算单词的开头而不是结尾 现在输入以下命令: ```cpp $ g++ -O3 Snippet20.cpp SnippetWC3.cpp Timer.cpp $ ./a.out data.txt ``` 我们收到以下输出: ![Figure 8.53: Output of the code in Snippet20.cpp ](img/C14583_08_53.jpg) ###### 图 8.53:snippet 20 . CPP 中代码的输出 现在输入以下命令: ```cpp $ g++ -O3 Snippet21.cpp SnippetWC3.cpp Timer.cpp $ ./a.out data.txt ``` 我们收到以下输出: ![Figure 8.54: Output of the code in Snippet21.cpp ](img/C14583_08_54.jpg) ###### 图 8.54:snippet 21 . CPP 中代码的输出 三者几乎同时运行,几毫秒的微小差异可以忽略不计。 22. 现在,我们可以解决字符串复制所花费的时间–我们将直接将文件读入字符串缓冲区,而不是使用`std::stringstream`(完整代码可在 **SnippetWC4.cpp** ): ```cpp string sContent; {   Timer t("String Alloc");   // Seek to end and reserve memory   ifs.seekg(0, std::ios::end);      sContent.resize(ifs.tellg()); } {   Timer t("File read");   // Seek back to start and read data   ifs.seekg(0, std::ios::beg);   ifs.read(&sContent[0], sContent.size()); } int wc; {   Timer t("Word count");   wc = wordCount(sContent); }   ``` 中找到) 23. Compile and run this version: ```cpp $ g++ -O3 Snippet21.cpp SnippetWC4.cpp Timer.cpp ``` 我们收到以下输出: ![Figure 8.55: Output of the code with changed file load code in SnippetWC4.cpp ](img/C14583_08_55.jpg) ###### 图 8.55:snippetwc4 . CPP 中文件加载代码改变后的代码输出 我们现在已经将文件读取代码所需的时间从大约 1,000 毫秒减少到 250 毫秒,提高了 4 倍。字数代码从大约`2500 毫秒`开始,减少到大约 60 毫秒——提高了 40 倍。整个程序的总性能提高了 3.6 倍。我们仍然可以问这是否是极限——事实上,TANSTATFC 仍然适用,并且还有一些事情可以做:使用`内存映射输入/输出`获得直接指向文件的缓冲区,而不是将数据读入`std::string`。这可能比分配和读取更快——它需要改变字数代码以接受一个`常量字符*`和一个长度,或者一个`标准::string_view`。使用不同的、更快的分配器来分配内存。使用`-3 月=原生`标志为原生 CPU 编译。然而,我们似乎不太可能从中获得非常大的性能提升,因为这些优化与字数统计算法本身无关。另一个最后的尝试可能是放弃 C++ 构造,使用`编译器内部函数`编写内联 SIMD 代码(这些函数被编译器直接翻译成单个汇编指令)。完成这项工作所需的知识超出了本介绍材料的范围。 24. Nevertheless, for the curious student, an `AVX2` (256-bit SIMD) version of `wordCount()` is provided (Snippet23.cpp). This version needs the input string to have a length that is a multiple of 32 and a space at the end. This means that the main function has to be rewritten (SnippetWC5.cpp): ```cpp $ g++ -O3 -march=native Snippet22.cpp SnippetWC5.cpp Timer.cpp $ ./a.out data.txt ``` 我们收到以下输出: ![Figure 8.56: Output of the code in Snippet22.cpp that uses SIMD intrinsics ](img/C14583_08_56.jpg) ###### 图 8.56:snippet 22 . CPP 中使用 SIMD 内函数的代码输出 请注意,我们需要使用`-march=native`标志,以便编译器使用 AVX SIMD 指令集。如果处理器不支持它,将导致编译错误。如果此可执行文件是为 AVX 目标编译的,并且在处理器不支持这些指令的系统上运行,程序会因“非法指令”异常而崩溃。这似乎是一个非常小的改进,但并不显著——用汇编程序或 SIMD 优化所需的努力和学习曲线通常太高,除非您的应用或行业有这些要求,否则是不合理的。SIMD 版本一次处理 32 个字节,但性能几乎没有提高。事实上,如果您使用编译器资源管理器检查其他代码片段中常规 C++ 实现的生成的汇编代码,您将看到编译器本身已经使用了 SIMD——这正好说明了编译器在让您的代码变快方面走了多远。 另一点需要注意的是,我们的文件读取和内存分配现在占用了大部分时间——抛开内存分配,我们可以得出结论,我们的代码已经成为 **I/O 绑定**而不是 **CPU 绑定**。这意味着无论我们写代码有多快,它都会受到获取数据速度的限制。我们从字数统计算法的一个非常简单的实现开始,增加了它的复杂性和速度,最后能够回到一个非常简单的实现,最终成为最快的。算法的整体速度提高了 40 倍。我们使用了许多方法,从稍微重新排列代码,到以不同的方式重新想象问题,再到执行微优化。没有一种方法可以一直有效,优化仍然是一种创造性的努力,需要想象力和技巧,通常还需要横向思维。随着编译器变得越来越聪明,超越它们变得越来越难——然而,程序员是唯一真正理解代码意图的人,而且总是有余地让代码变得更快。 ### 活动 1:优化拼写检查算法 在本活动中,我们将尝试逐步优化程序。这个活动是关于一个简单的拼写检查器,它获取一个字典和一个文本文件,并打印出字典中没有的文本单词列表。在 **Speller.cpp** 中提供了一个基本的框架程序,以及一个示例字典和文本文件,分别为 **dict.txt** 和 **data.txt** 。提供了一个名为 out.txt 的文件,其中包含程序的所需输出(拼写错误单词的索引列表)。文本文件存在于`7zip 存档`中,即`活动 1.7z`。 该词典取自许多 Linux 发行版提供的 Linux 单词列表。文本文件与我们在上一个练习中使用的文件相似——它与我们在字数统计练习中使用的文件一样大,所有标点符号都被删除并转换为小写。 请注意,字典只是一个例子,所以不要假设所有有效的单词都存在于其中——输出中的许多单词很可能是拼写正确的单词。框架代码读取字典和文本文件,并调用上面的拼写检查代码(您将编写)。之后,它将结果输出与 **out.txt** 的内容进行比较,并打印程序是否按预期工作。执行拼写检查的函数返回字典中没有的单词的索引向量。因为我们只关注拼写检查算法,所以只有代码是定时的。不考虑读取文件和比较输出所花费的时间。您将开发该程序的更快版本-参考文件夹中提供的参考实现有 **Speller1.cpp** 、 **Speller2.cpp** 等。 在每一步中,您将只得到提示,告诉您应该更改什么以使其更快–只有`get 拼错()`函数中的代码将被修改,而不是任何其他代码。只要代码产生正确的结果,并且在`main()`内的代码没有改变,学生可以按照自己的意愿自由执行代码。 #### 注意 优化是一个创造性的、非确定性的过程——学生不能保证也不总是可能得出与参考实现相同的代码。如果您编写的代码的性能不如引用实现,这应该不会令人惊讶。事实上,甚至有可能您的代码比引用更快。 执行以下步骤来实施本活动: 制作一个名为 Speller1.cpp 的 Speller.cpp 的副本,并实现`get 拼错()`函数的代码。使用`std::set`及其`count()`方法实现。 1. Write the next version of the program as Speller2.cpp, and then compile it and time it as before. Try using `std::unordered_set` rather than `std::set`. You should get about a 2x speedup with this implementation. 在最终版本 **Speller3.cpp** 中,使用**布隆过滤器**数据结构来实现拼写检查算法。用不同数量的散列函数和不同大小的布隆过滤器进行实验,看看什么效果最好。 2. For each of the preceding steps, compile the program and run it as follows (change the input file name as required): ```cpp $ g++ -O3 Speller1.cpp Timer.cpp $ ./a.out ``` #### 注意 您不应该期望时间完全如这里所示,但是如果您正确地实现了代码,速度的相对提高应该接近我们在这里看到的。 对每个步骤执行上述命令后,将生成以下输出。输出将显示代码的时序,如果输出正确,还会显示一条初始消息。以下是步骤 1 的输出: ![Figure 8.57: Example output of the code for Step 1 ](img/C14583_08_57.jpg) ###### 图 8.57:步骤 1 的代码输出示例 以下是步骤 2 的输出: ![Figure 8.58: Example output of the code for Step 2 ](img/C14583_08_58.jpg) ###### 图 8.58:步骤 2 的代码输出示例 以下是步骤 3 的输出: ![Figure 8.59: Example output of the code for Step 3 ](img/C14583_08_59.jpg) ###### 图 8.59:步骤 3 的代码输出示例 #### 注意 这项活动的解决方案可以在第 725 页找到。 ## 总结 在这一章中,我们已经讨论了许多复杂的材料。对于任何现代 C++ 开发人员来说,优化代码都是一项困难但必要的技能。机器学习、超现实游戏、大数据分析和高能效计算的需求使得这对于任何 C++ 专业人员来说都是一个非常重要的学习领域。我们了解到性能优化的过程分为两个阶段。 首先,优化从适当的性能测量策略开始,测试条件反映真实世界的数据和使用模式。我们已经学习了如何通过各种方法来衡量性能——学习汇编代码、手动计时、源代码插装以及使用运行时分析器。一旦我们有了准确的测量,我们就能真正理解我们程序的哪些部分实际上是慢的,并集中精力在那里获得最大的改进。第二个阶段涉及实际修改程序——我们了解了几种策略,从为代码使用最佳编译器选项开始,使用并行化特性,还使用配置文件数据来帮助编译器,然后是一些简单的代码转换,这些转换可以在不进行重大代码更改的情况下产生微小但有用的性能提升。然后,我们学习了如何通过构造循环和条件来提高性能,使代码对分支预测更加友好。 然后,我们了解了缓存对性能的显著影响,并研究了一些技术,如 SOA 模式,以使我们的代码利用现代 CPU 中的缓存。最后,我们把所有这些东西放在一起,作为一个单词计数程序和简单拼写检查器的真实例子,来实践我们所学的东西。除了本章的材料之外,还有很多其他的高级技术和理论需要学习,但是我们在这里所涵盖的应该会给任何学生未来的学习打下坚实的基础。 在这些章节的最后,您已经探索了许多与使用高级 C++ 相关的主题。在前几章中,您已经学习了如何编写可移植的软件,如何使用模板来使用类型系统,以及如何有效地使用指针和继承。然后,您已经探索了 C++ 标准库,包括流和并发,它们是构建大型现实世界应用的基本工具。在最后几节中,您学习了如何测试和调试程序,以及如何优化代码以高效运行。在广泛使用的编程语言中,C++ 可能是最复杂的,也是最具表现力的。这本书只是一个开始,它会给你一个坚实的平台来继续你的进一步学习。 ================================================ FILE: docs/adv-cpp/10.md ================================================ # 十、附录 ## 关于 包括这一部分是为了帮助学生完成书中的活动。它包括学生为实现活动目标而要执行的详细步骤。 ## 第 1 章 -可移植 C++ 软件剖析 ### 活动 1:向项目添加新的源文件-头文件对 在本练习中,我们将创建一个新的源文件-头文件对,其中包含一个名为`sum`的新函数。它接受两个参数并返回它们的和。该文件对将被添加到现有项目中。按照以下步骤实施本活动: 1. First, open the Eclipse IDE with the existing project that we created in *Exercise 3*, *Adding New Source Files to CMake and Eclipse CDT*. Right-click on the **src** folder in the **Project Explorer** pane. #### 注意 我们可以创建`。cpp`和`。h`单独文件或使用新建类向导,稍后删除类代码。使用新建类向导很方便,因为它还可以创建有用的样板代码。 2. 从弹出菜单中选择**新建** | **类**。键入`SumFunc`,点击**完成**按钮。 3. Next, edit the `SumFunc.h` file to look like the following code: ```cpp #ifndef SRC_SUMFUNC_H_ #define SRC_SUMFUNC_H_ int sum(int a, int b); #endif /* SRC_SUMFUNC_H_ */ ``` 请注意,我们实际上会删除该类,而是提供一个函数。我们可以分别创建这两个文件。然而,`add 类`函数创建了它们,并添加了一些我们将使用的样板代码。在这里,我们的文件以`include` guard 开始和结束,这是防止双重包含问题的常见策略。我们有函数的正向声明,它允许其他文件在包含这个头文件后调用函数。 4. Next, edit the `SumFunc.cpp` file as illustrated here: ```cpp #include "SumFunc.h" #include int sum(int a, int b) {   return a + b; } ``` 在这个文件中,我们包含了头文件,并提供了函数的主体,它将两个给定的整数相加并返回。 5. Edit the `CMakeFiles.txt` file so that its `add_executable` section reflects the following code: ```cpp add_executable(CxxTemplate   src/CxxTemplate.cpp     src/ANewClass.cpp   src/SumFunc.cpp ) ``` 这里,我们将`src/SumFunc.cpp`文件添加到可执行源文件列表中,以便将其链接到可执行文件中。 6. Make the following changes in `CxxTemplate.cpp`: ```cpp #include "CxxTemplate.h" #include "ANewClass.h" #include "SumFunc.h" //add this line ... CxxApplication::CxxApplication( int argc, char *argv[] ) {   std::cout << "Hello CMake." << std::endl;   ANewClass anew;   anew.run();   std::cout << sum(3, 4) << std::endl; // add this line } ``` #### 注意 这个文件的完整代码可以在这里找到:[https://github . com/trainingypbackt/Advanced-CPlusPlus/blob/master/lesson 1/activity 01/src/cxxtemplate . CPP](https://github.com/TrainingByPackt/Advanced-CPlusPlus/blob/master/Lesson1/Activity01/src/CxxTemplate.cpp)。 在这里,我们添加了一行,其中我们用`3`和`4`调用`求和`函数,并将结果打印到控制台。 7. 构建并运行项目(**项目** | **构建所有** | **运行** | **运行**)。您看到的输出应该如下所示: ![Figure 1.57: The output ](img/C14583_01_57.jpg) ###### 图 1.57:输出 通过本练习,您练习了向项目中添加一个新的源文件-头文件对。这些文件对是 C++ 开发中非常常见的模式。他们可以主持全球性的活动,就像我们在这次活动中所做的那样。更常见的是,它们托管类及其定义。在整个开发过程中,您将向应用中添加更多的头文件对。因此,重要的是要习惯于添加它们,不要拖拖拉拉,这会导致难以维护和测试的大型单片文件。 ### 活动 2:添加新类及其测试 在本练习中,我们将添加一个模拟`1D`直线运动的新类。该类将具有用于`位置`和`速度`的双字段。它还将有一个`advanceTimeBy()`方法,该方法接收一个双`dt`参数,该参数基于`速度`的值修改`位置`。双数值用`EXPECT_DOUBLE_EQ`代替`EXPECT_EQ`。在本活动中,我们将向项目中添加一个新类及其测试。按照以下步骤执行本活动: 1. 用我们现有的项目打开 Eclipse 集成开发环境。要创建新类,右键单击**项目浏览器**窗格中的 **src** 文件夹,并选择**新建** | **类**。键入`LinearMotion1D`作为名称并创建类。 2. 打开我们在上一步中创建的`线性运动 1D.h`文件。将`位置`和`速度` `双`场加入其中。另外,将正向引用添加到`高级时间比`方法中,该方法将一个`双 dt`变量作为参数。构造函数和析构函数已经在类中了。以下是`线性运动 1D.h`中这些变化的最终结果: ```cpp #ifndef SRC_LINEARMOTION1D_H_ #define SRC_LINEARMOTION1D_H_ class LinearMotion1D { public:   double position;   double velocity;   void advanceTimeBy(double dt);   LinearMotion1D();   virtual ~LinearMotion1D(); }; #endif /* SRC_LINEARMOTION1D_H_ */ ``` 3. 现在打开`LinearMotion1D.cpp`并添加`advanced time by`方法的实现。我们的`速度`是我们班的一个场,时差是这个方法的一个参数。`位置`的变化等于`速度`乘以时间变化,所以我们计算结果并将其添加到`位置`变量中。我们还使用现有的构造器代码将`位置`和`速度`初始化为 0。以下是`LinearMotion1D.cpp`中这些变化的最终结果: ```cpp #include "LinearMotion1D.h" void LinearMotion1D::advanceTimeBy(double dt) {   position += velocity * dt; } LinearMotion1D::LinearMotion1D() {   position = 0;   velocity = 0; } LinearMotion1D::~LinearMotion1D() { } ``` 4. 为此类创建一个测试。右键点击**测试**文件夹,选择**新建** | **源文件**。键入`linear motion 1 test . CPP`作为名称并创建它。 5. 现在打开`线性运动 1 测试. cpp`。为左右两个不同方向的运动创建两个测试。对于它们中的每一个,创建一个`线性运动 1D`对象,初始化它的位置和速度,并调用`提前时间`来实际发生运动。然后,检查它是否移动到了我们预期的位置。以下是`linemotion1 test . CPP`中这些变化的最终结果: ```cpp #include "gtest/gtest.h" #include "../src/LinearMotion1D.h" namespace { class LinearMotion1DTest: public ::testing::Test {}; TEST_F(LinearMotion1DTest, CanMoveRight) {   LinearMotion1D l;   l.position = 10;   l.velocity = 2;   l.advanceTimeBy(3);   EXPECT_DOUBLE_EQ(16, l.position); } TEST_F(LinearMotion1DTest, CanMoveLeft) {   LinearMotion1D l;   l.position = 10;   l.velocity = -2;   l.advanceTimeBy(3);   EXPECT_DOUBLE_EQ(4, l.position); } } ``` 6. 现在修改我们的 CMake 配置文件,这样我们生成的这些源文件也可以使用。对于`线性运动 1D`类,添加其`。cpp`文件作为可执行文件,以便它被编译并与其他源文件链接在一起。以下是`CMakeLists.txt`的`add _ executive`部分变成的内容: ```cpp add_executable(CxxTemplate   src/CxxTemplate.cpp     src/ANewClass.cpp   src/SumFunc.cpp   src/LinearMotion1D.cpp # added ) ``` 7. 对于我们刚刚创建的测试,编辑**测试/CMakeLists.txt** 。在这里,我们需要添加测试源文件`linear motion 1 test . CPP`,以及它所使用的类的源文件`LinearMotion1D.cpp`。因为它们在不同的目录中,所以作为`访问它们../src/LinearMotion1D.cpp`。以下是`测试/CMakeLists.txt`的`add _ executive`部分变成的内容: ```cpp add_executable(tests   CanTest.cpp   SumFuncTest.cpp   ../src/SumFunc.cpp   LinearMotion1DTest.cpp # added   ../src/LinearMotion1D.cpp # added ) ``` 8. 构建项目并运行测试。我们将看到所有测试都是成功的: ![Figure 1.58: All tests are successful ](img/C14583_01_58.jpg) ###### 图 1.58:所有测试都成功 在本活动中,您执行了向项目中添加新类及其测试的任务。您创建了一个模拟一维运动的类,并编写了单元测试来确保它正常工作。 ### 活动 3:提高代码可读性 在本练习中,您将练习提高给定代码的质量。按照以下步骤实施本活动: 1. 打开 Eclipse CDT,在 Eclipse 中的一个源文件对中创建一个类。为此,右键单击**项目浏览器**中的 **src** 文件夹。从弹出菜单中选择**新** | **类**。 2. 输入`速度计算器`作为标题文件名,点击**完成**。它将创建两个文件:**速度计算器. h** 和**速度计算器. cpp** 。我们为上面的两个文件提供了代码。添加为每个文件提供的代码。 3. 现在我们需要将该类添加到 CMake 项目中。打开项目根目录下的 **CMakeLists.txt** 文件(在 **src** 文件夹外),并在文件中进行以下更改: ```cpp   src/LinearMotion1D.cpp   src/SpeedCalculator.cpp # add this line ) ``` 4. 现在选择**文件** | **全部保存**保存所有文件,通过选择**项目** | **全部建立**来建立项目。确保没有错误。 5. 在我们的`main()`函数中创建一个`速度计算器`类的实例,并调用其`run()`方法。打开 **CxxTemplate.cpp** 并包含我们的新类,然后通过添加以下代码编辑`主`功能: ```cpp #include "SpeedCalculator.h" int main( int argc, char *argv[] ) {   cxxt::CxxApplication app( argc, argv );   // add these three lines   SpeedCalculator speedCalculator;   speedCalculator.initializeData(10);   speedCalculator.calculateAndPrintSpeedData();   return 0; } ``` 6. 要修复样式,只需使用 **Source** | **Format** 并选择格式化整个文件。幸运的是,变量名没有任何问题。 7. Simplify the code to make it more understandable. The loop in `calculateAndPrintSpeedData` is doing a couple of things at the same time. It's calculating the speed, finding the minimum and maximum values of it, checking whether we crossed a threshold, and storing the speed. If the speed was a transient value, taking it apart would mean storing it somewhere to loop on it one more time. However, since we are storing it in the speeds array anyway, we can loop one more time on it for clarity of code. Here is the updated version of the loop: ```cpp 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;   speeds[i] = speed; } for (int i = 0; i < numEntries; ++ i) {   double speed = speeds[i];   if (maxSpeed < speed) {     maxSpeed = speed;   }   if (minSpeed > speed) {     minSpeed = speed;   } } for (int i = 0; i < numEntries; ++ i) {   double speed = speeds[i];   double dt = timesInSeconds[i + 1] - timesInSeconds[i];   if (speed > speedLimit) {     limitCrossDuration += dt;   } } ``` 这多少有点味道的问题,但是将循环的大**变轻有助于可读性。此外,它分离了任务,并消除了它们在循环迭代期间相互交互的可能性。第一个循环创建并保存速度值。第二个循环找到最小和最大速度值。第三个循环决定了超过限速多长时间。请注意,这是一个效率稍低的实现;然而,它清楚地分离了所采取的行动,我们不必在循环的长时间迭代中在精神上分离离散的行动。** 8. Run the preceding code and observe the problem at runtime. While the code is better in terms of style now, it suffers from several mistakes, some of which will create runtime errors. First, when we run the application, we see the following output in Eclipse: ![Figure 1.59: Program output in Eclipse CDT ](img/C14583_01_59.jpg) ###### 图 1.59:Eclipse CDT 中的程序输出 注意**退出值:-1** 在顶部。当这不是`0`时,表示我们的代码有问题。 9. Execute the program manually in the console. Here's the output we get: ![Figure 1.60: Program output in the terminal with the error ](img/C14583_01_60.jpg) ###### 图 1.60:终端中有错误的程序输出 不幸的是,我们在 Eclipse 中没有得到分段错误输出,因此您必须在 Eclipse 控制台视图中检查退出值。为了找到问题,我们将在下一步中使用调试器。 10. Press the debug toolbar button in Eclipse to start the application in debug mode. Press the resume button to continue execution. It will stop at line 40 of `SpeedCalculator.cpp`, right when an error is about to happen. If you hover over `speeds`, you realize that it is an invalid memory reference: ![Figure 1.61: Invalid memory reference ](img/C14583_01_61.jpg) ###### 图 1.61:无效的内存引用 11. 进一步检查后,我们意识到我们从未初始化过任何东西的`速度`指针。在我们的速度计算器功能中为它分配内存: ```cpp void SpeedCalculator::calculateAndPrintSpeedData() {   speeds = new double[numEntries]; // add this line   double maxSpeed = 0; ``` 12. Run it again. We get the following output: ```cpp Hello CMake. Hello from ANewClass. 7 CxxTemplate: SpeedCalculator.cpp:38: void SpeedCalculator::calculateAndPrintSpeedData(): Assertion `dt > 0' failed. ``` 请注意,这是一个断言,即代码必须确保计算出的`dt`始终大于零。这是我们确信的事情,我们希望它能帮助我们在开发过程中发现错误。Assert 语句在生产构建中被忽略,因此您可以将它们随意地放在代码中,作为在开发过程中捕捉错误的保护措施。尤其是因为与高级语言相比,C++ 缺少很多安全检查,在潜在的不安全代码中放置`断言`语句有助于捕捉错误。 13. Let's investigate why our `dt` ended up not larger than zero. For this, we fire up the debugger again. It stops at this strange place: ![Figure 1.62: Debugger stopped at a library without source code ](img/C14583_01_62.jpg) ###### 图 1.62:调试器在没有源代码的库中停止 14. The actual error is raised deep inside a library. However, our own functions are still on the stack and we can investigate their state at that time. Click on **SpeedCalculator** above **main** in the tree to the left: ![Figure 1.63: Value of dt as the program is running ](img/C14583_01_63.jpg) ###### 图 1.63:程序运行时的 dt 值 看来我们的`dt`在这里变成了 **-43** (确切数值不重要)。查看**变量**视图,我们意识到`i`是 **9** ,这是我们输入数组的最后一个元素: ![Figure 1.64: Values of variables ](img/C14583_01_64.jpg) ###### 图 1.64:变量值 这感觉像是一个边界问题。仔细查看代码,我们意识到我们使用的是`timeseconds[10]`,这是数组中不存在的第十一个元素。进一步思考,我们意识到,当我们有 10 个位置时,我们只能有 9 个位置对减法,从而有 9 个速度。这是一个非常常见且难以捕捉的错误,因为 C++ 不会强制您留在数组中。 15. Rework our whole code for this problem: ```cpp void SpeedCalculator::calculateAndPrintSpeedData() {   speeds = new double[numEntries - 1];   double maxSpeed = 0; ...   for (int i = 0; i < numEntries - 1; ++ i) {     double dt = timesInSeconds[i + 1] - timesInSeconds[i]; ...   for (int i = 0; i < numEntries - 1; ++ i) {     double speed = speeds[i]; ....   for (int i = 0; i < numEntries - 1; ++ i) {     double speed = speeds[i]; ``` 最后,我们的代码似乎运行没有任何错误,正如我们在下面的输出中看到的: ![Figure 1.65: Program output ](img/C14583_01_65.jpg) ###### 图 1.65:程序输出 16. However, there is a curious point here: **Min speed** is always `0`, no matter how many times you run it. To investigate, let's put a breakpoint at the following line: ![Figure 1.66: Placing a breakpoint ](img/C14583_01_66.jpg) ###### 图 1.66:放置断点 17. When we debug our code, we see that it never stops here. This is obviously wrong. Upon further investigation, we realize that `minSpeed` is initially 0, and every other speed value is larger than that. We should initialize it to either something very large, or we need to get the very first element as the minimum value. Here, we choose the second approach: ```cpp for (int i = 0; i < numEntries - 1; ++ i) {   double speed = speeds[i];   if (i == 0 || maxSpeed < speed) { // changed     maxSpeed = speed;   }   if (i == 0 || minSpeed > speed) { // changed     minSpeed = speed;   } } ``` 而`maxSpeed`不需要这个,保持一致就好。现在当我们运行代码时,我们看到我们不再将`0`作为我们的最小速度: ![Figure 1.67: Program output ](img/C14583_01_67.jpg) ###### 图 1.67:程序输出 18. Our code seems to be running fine. However, there is another mistake that we have made. When we debug our code, we see that our first elements are not zero: ![Figure 1.68: Values of variables ](img/C14583_01_68.jpg) ###### 图 1.68:变量值 19. The pointer dereferenced the first element in the array. We had initialized elements to zero here, but they do not seem to be zero. Here is the updated code: ```cpp   // add these two lines:   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);   } ``` 当我们调查时,我们意识到我们从零开始循环并覆盖第一个项目。此外,我们试图访问`位置【0-1】`,这是一个错误,也是 C++ 不强制数组边界的另一个例子。当我们让循环从 1 开始时,所有这些问题都消失了: ```cpp   timesInSeconds[0] = 0.0;   positions[0] = 0.0;   for (int i = 1; i < numEntries; ++ i) {     positions[i] = positions[i - 1] + (rand() % 500);     timesInSeconds[i] = timesInSeconds[i - 1] + ((rand() % 10) + 1);   } ``` 以下是用更新后的代码生成的输出: ![Figure 1.69: Program output ](img/C14583_01_69.jpg) ###### 图 1.69:程序输出 仅仅通过查看这段代码,我们无法区分。这些都是随机值,看起来和以前没什么不同。这样的 bug 很难发现,并且会导致随机行为,给我们留下难以跟踪的错误。您可以做些什么来避免这样的错误,包括在解引用指针时格外小心,尤其是在循环中;将代码分成函数,并为它们编写单元测试;并且自由地使用`断言`语句来执行编译器或运行时不执行的事情。 ## 2A 章-不允许养鸭-类型和推导 ### 活动 1:图形处理 在本练习中,我们将实现两个类(`点 3d`和`矩阵 3d`,以及乘法运算符,以便我们可以平移、缩放和旋转点。我们还将实现一些辅助方法,为转换创建必要的矩阵。按照以下步骤实施本活动: 1. Load the prepared project from the **Lesson2A/Activity01** folder and configure the Current Builder for the project to be `CMake Build (Portable)`. Build and configure the launcher and run the unit tests (which fail). Recommend that the name that's used for the test runner is `L2AA1graphicstests`. #### CMake 配置 按照*练习 1* 、*声明变量和探索大小*的*步骤 9* ,将项目配置为一个 CMake 项目。 2. 为`点 3d`类添加一个测试,以验证默认构造函数创建了一个`原点【0,0,0,1】`。 3. 打开**点 3 tests . CPP**文件,在顶部添加以下一行。 4. Replace the failing existing test with the following test: ```cpp TEST_F(Point3dTest, DefaultConstructorIsOrigin) {     Point3d pt;     float expected[4] = {0,0,0,1};     for(size_t i=0 ; i < 4 ; i++)     {         ASSERT_NEAR(expected[i], pt(i), Epsilon) << "cell [" << i << "]";     } } ``` 这个测试需要我们写一个访问操作符。 5. Replace the current class definition in **point3d.hpp** file with the following code: ```cpp include class Point3d { public:     static constexpr size_t NumberRows{4};     float operator()(const int index) const     {         return m_data[index];     } private:     float m_data[NumberRows]; }; ``` 测试现在构建并运行,但是失败了。 6. 将默认构造函数的声明添加到`点 3d`声明: ```cpp Point3d(); ``` 7. Add the implementation to the **point3d.cpp** file: ```cpp Point3d::Point3d() {     for(auto& item : m_data)     {         item = 0;     }     m_data[NumberRows-1] = 1; } ``` 测试现在构建、运行并通过。 8. Add the next test: ```cpp TEST_F(Point3dTest, InitListConstructor3) {     Point3d pt {5.2, 3.5, 6.7};     float expected[4] = {5.2,3.5,6.7,1};     for(size_t i=0 ; i < 4 ; i++)     {         ASSERT_NEAR(expected[i], pt(i), Epsilon) << "cell [" << i << "]";     } } ``` 这个测试无法编译。因此,我们需要实现另一个构造函数——以`STD::initializer _ list<>`为参数的构造函数。 9. 在头文件中添加以下内容: ```cpp #include ``` 10. 将以下构造函数声明添加到头文件中的 Point3d 类: ```cpp Point3d(std::initializer_list list); ``` 11. Add the following code to the implementation file. This code ignores error handling, which will be added in *Lesson 3*, *The Distance Between Can and Should – Objects, Pointers, and Inheritance*: ```cpp Point3d::Point3d(std::initializer_list list) {     m_data[NumberRows-1] = 1;     int i{0};     for(auto it1 = list.begin();         i void VerifyPoint(Point3d& pt, float (&expected)[size]) {     for(size_t i=0 ; i< size ; i++)     {         ASSERT_NEAR(expected[i], pt(i), Epsilon) << "cell [" << i << "]";     } } ``` 14. This now means that the last test can be rewritten as follows: ```cpp TEST_F(Point3dTest, InitListConstructor4) {     Point3d pt {5.2, 3.5, 6.7, 2.0};     float expected[4] = {5.2,3.5,6.7,2.0};     VerifyPoint(pt, expected); } ``` 保持测试的可读性与产品代码的可读性一样重要。 15. 接下来,通过以下测试添加对等式和不等式运算符的支持: ```cpp TEST_F(Point3dTest, EqualityOperatorEqual) {     Point3d pt1 {1,3,5};     Point3d pt2 {1,3,5};     ASSERT_EQ(pt1, pt2); } TEST_F(Point3dTest, EqualityOperatorNotEqual) {     Point3d pt1 {1,2,3};     Point3d pt2 {1,2,4};     ASSERT_NE(pt1, pt2); } ``` 16. 为了实现这些,在头文件中添加以下声明/定义: ```cpp bool operator==(const Point3d& rhs) const; bool operator!=(const Point3d& rhs) const {     return !operator==(rhs); } ``` 17. 现在,在。cpp 文件: ```cpp bool Point3d::operator==(const Point3d& rhs) const {     for(int i=0 ; i   // Required for size_t definition class Matrix3d { public:     static constexpr size_t NumberRows{4};     static constexpr size_t NumberColumns{4};     Matrix3d();     float operator()(const int row, const int column) const     {     return m_data[row][column];     } private:     float m_data[NumberRows][NumberColumns]; }; ``` 31. 更新 **matrix3d.cpp** 文件以使用常量: ```cpp Matrix3d::Matrix3d() {     for (int i{0} ; i< NumberRows ; i++)         for (int j{0} ; j< NumberColumns ; j++)             m_data[i][j] = (i==j); } ``` 32. 重建测试,并确保它们仍然通过。 33. 现在,我们需要添加初始化列表构造函数。为此,添加以下测试: ```cpp TEST_F(Matrix3dTest, InitListConstructor) {     Matrix3d mat{ {1,2,3,4}, {5,6,7,8},{9,10,11,12}, {13,14,15,16}};     int expected{1};     for( int row{0} ; row<4 ; row++)         for( int col{0} ; col<4 ; col++, expected++)         {             ASSERT_FLOAT_EQ(expected, mat(row,col)) << "cell[" << row << "][" << col << "]";         } } ``` 34. 添加初始化列表支持的包含文件,并在 **matrix3d.hpp** : ```cpp #include class Matrix3d { public:     Matrix3d(std::initializer_list> list); ``` 中声明构造函数 35. 最后,将构造函数的实现添加到。cpp 文件: ```cpp Matrix3d::Matrix3d(std::initializer_list> list) {     int i{0};     for(auto it1 = list.begin(); ibegin(); j ``` 51. 开始添加转换矩阵工厂方法。使用以下测试,我们将开发各种工厂方法(测试应一次添加一个): ```cpp TEST_F(Matrix3dTest, CreateTranslateIsCorrect) {     Matrix3d mat = createTranslationMatrix(-0.5, 2.5, 10.0);     Matrix3d expected {{1.0, 0.0, 0.0, -0.5},                        {0.0, 1.0, 0.0, 2.5},                        {0.0, 0.0, 1.0, 10.0},                        {0.0, 0.0, 0.0, 1.0}     };     VerifyMatrixResult(expected, mat); } TEST_F(Matrix3dTest, CreateScaleIsCorrect) {     Matrix3d mat = createScaleMatrix(3.0, 2.5, 11.0);     Matrix3d expected {{3.0, 0.0,  0.0, 0.0},                        {0.0, 2.5,  0.0, 0.0},                        {0.0, 0.0, 11.0, 0.0},                        {0.0, 0.0,  0.0, 1.0}     };     VerifyMatrixResult(expected, mat); } TEST_F(Matrix3dTest, CreateRotateX90IsCorrect) {     Matrix3d mat = createRotationMatrixAboutX(90.0F);     Matrix3d expected {{1.0, 0.0,  0.0, 0.0},                        {0.0, 0.0, -1.0, 0.0},                        {0.0, 1.0,  0.0, 0.0},                        {0.0, 0.0,  0.0, 1.0}     };     VerifyMatrixResult(expected, mat); } TEST_F(Matrix3dTest, CreateRotateX60IsCorrect) {     Matrix3d mat = createRotationMatrixAboutX(60.0F);     float sqrt3_2 = static_cast(std::sqrt(3.0)/2.0);     Matrix3d expected {{1.0, 0.0,     0.0,     0.0},                        {0.0, 0.5,    -sqrt3_2, 0.0},                        {0.0, sqrt3_2,  0.5,    0.0},                        {0.0, 0.0,     0.0,     1.0}     };     VerifyMatrixResult(expected, mat); } TEST_F(Matrix3dTest, CreateRotateY90IsCorrect) {     Matrix3d mat = createRotationMatrixAboutY(90.0F);     Matrix3d expected {{0.0, 0.0,  1.0, 0.0},                        {0.0, 1.0,  0.0, 0.0},                        {-1.0, 0.0, 0.0, 0.0},                        {0.0, 0.0,  0.0, 1.0}     };     VerifyMatrixResult(expected, mat); } TEST_F(Matrix3dTest, CreateRotateY60IsCorrect) {     Matrix3d mat = createRotationMatrixAboutY(60.0F);     float sqrt3_2 = static_cast(std::sqrt(3.0)/2.0);     Matrix3d expected {{0.5,      0.0,   sqrt3_2,  0.0},                        {0.0,      1.0,    0.0,     0.0},                        {-sqrt3_2, 0.0,    0.5,     0.0},                        {0.0,      0.0,    0.0,     1.0}     };     VerifyMatrixResult(expected, mat); } TEST_F(Matrix3dTest, CreateRotateZ90IsCorrect) {     Matrix3d mat = createRotationMatrixAboutZ(90.0F);     Matrix3d expected {{0.0, -1.0,  0.0, 0.0},                        {1.0, 0.0,  0.0, 0.0},                        {0.0, 0.0,  1.0, 0.0},                        {0.0, 0.0,  0.0, 1.0}     };     VerifyMatrixResult(expected, mat); } TEST_F(Matrix3dTest, CreateRotateZ60IsCorrect) {     Matrix3d mat = createRotationMatrixAboutZ(60.0F);     float sqrt3_2 = static_cast(std::sqrt(3.0)/2.0);     Matrix3d expected {{0.5,     -sqrt3_2,   0.0,  0.0},                        {sqrt3_2,      0.5,   0.0,  0.0},                        {0.0,          0.0,   1.0,  0.0},                        {0.0,          0.0,   0.0,  1.0}     };     VerifyMatrixResult(expected, mat); } ``` 52. 向 matrix3d 头文件添加以下声明: ```cpp Matrix3d createTranslationMatrix(float dx, float dy, float dz); Matrix3d createScaleMatrix(float sx, float sy, float sz); Matrix3d createRotationMatrixAboutX(float degrees); Matrix3d createRotationMatrixAboutY(float degrees); Matrix3d createRotationMatrixAboutZ(float degrees); ``` 53. 在 matrix3d 实现文件的顶部,添加`#include < cmath >`。 54. 最后,将以下实现添加到`matrix3d`实现文件中: ```cpp Matrix3d createTranslationMatrix(float dx, float dy, float dz) {     Matrix3d matrix;     matrix(0, 3) = dx;     matrix(1, 3) = dy;     matrix(2, 3) = dz;     return matrix; } Matrix3d createScaleMatrix(float sx, float sy, float sz) {     Matrix3d matrix;     matrix(0, 0) = sx;     matrix(1, 1) = sy;     matrix(2, 2) = sz;     return matrix; } Matrix3d createRotationMatrixAboutX(float degrees) {     Matrix3d matrix;     double pi{4.0F*atan(1.0F)};     double radians = degrees / 180.0 * pi;     float cos_theta = static_cast(cos(radians));     float sin_theta = static_cast(sin(radians));     matrix(1, 1) =  cos_theta;     matrix(2, 2) =  cos_theta;     matrix(1, 2) = -sin_theta;     matrix(2, 1) =  sin_theta;     return matrix; } Matrix3d createRotationMatrixAboutY(float degrees) {     Matrix3d matrix;     double pi{4.0F*atan(1.0F)};     double radians = degrees / 180.0 * pi;     float cos_theta = static_cast(cos(radians));     float sin_theta = static_cast(sin(radians));     matrix(0, 0) =  cos_theta;     matrix(2, 2) =  cos_theta;     matrix(0, 2) =  sin_theta;     matrix(2, 0) = -sin_theta;     return matrix; } Matrix3d createRotationMatrixAboutZ(float degrees) {     Matrix3d matrix;     double pi{4.0F*atan(1.0F)};     double radians = degrees / 180.0 * pi;     float cos_theta = static_cast(cos(radians));     float sin_theta = static_cast(sin(radians));     matrix(0, 0) =  cos_theta;     matrix(1, 1) =  cos_theta;     matrix(0, 1) = -sin_theta;     matrix(1, 0) =  sin_theta;     return matrix; } ``` 55. 为了编译并通过测试,我们需要在`matrix3d` : ```cpp float& operator()(const int row, const int column) {     return m_data[row][column]; } ``` 的声明中增加一个访问器 56. 再次构建并运行所有测试,以显示它们都通过了。 57. 在`point3d.hpp`中,添加`< ostream >`的 include,并在末尾的 point3d 类中添加以下好友声明: ```cpp friend std::ostream& operator<<(std::ostream& , const Point3d& ); ``` 58. 在类后写操作符的内联实现: ```cpp inline std::ostream& operator<<(std::ostream& os, const Point3d& pt) {     const char* sep = "[ ";     for(auto value : pt.m_data)     {         os << sep  << value;         sep = ", ";     }     os << " ]";     return os; } ``` 59. 打开 **main.cpp** 文件,删除行: ```cpp //#define ACTIVITY1 ``` 中的注释分隔符//号 60. 构建并运行名为`图形`的应用–您需要创建一个新的运行配置。如果您对`点 3d`和`矩阵 3d`的实现是正确的,那么程序将显示以下输出: ![](img/C14583_02A_53.jpg) ###### 图 2A.53:成功运行活动程序 在本练习中,我们实现了两个类,它们构成了实现三维图形渲染所需的所有操作的基础。我们使用运算符重载来实现这一点,这样 Matrix3d 和 Point3d 就可以像本地类型一样使用。这可以很容易地扩展到处理点的向量,如果我们希望操纵整个对象,这是必需的。 ## 章节-2B-不允许鸭子-模板和演绎 ### 活动 1:开发通用“包含”模板函数 在本练习中,我们将实现几个助手类,用于检测`std::string`类案例和`std::set`案例,然后使用它们为特定容器定制 contains 函数。按照以下步骤实施本活动: 1. 从**第 2B 课/练习 01** 文件夹加载准备好的项目。构建和配置启动器并运行单元测试(未通过一个虚拟测试)。我们建议测试跑者的名字是`L2BA1tests`。 2. Open the **containsTests.cpp** file and replace the existing test with the following: ```cpp TEST_F(containsTest, DetectNpos) {     ASSERT_TRUE(has_npos_v);     ASSERT_FALSE(has_npos_v>);     ASSERT_FALSE(has_npos_v>); } ``` 这个测试要求我们编写一组帮助器模板来检测容器类是否支持一个名为 NPO 的静态成员变量。 3. Add the following code to the **contains.hpp** file: ```cpp template auto test_npos(int) -> decltype((void)T::npos, std::true_type{}); template auto test_npos(long) -> std::false_type; template struct has_npos : decltype(test_npos(0)) {}; template< class T > inline constexpr bool has_npos_v = has_npos::value; ``` 测试现在运行并通过。 4. Add the following tests to the **containsTest.cpp** file: ```cpp TEST_F(containsTest, DetectFind) {     ASSERT_TRUE((has_find_v));     ASSERT_TRUE((has_find_v, int>));     ASSERT_FALSE((has_find_v, int>)); } ``` 这个测试需要我们编写一组帮助器模板来检测容器类是否有一个接受一个参数的`find()`方法。 5. Add the following code to the **contains.hpp** file: ```cpp template auto test_find(int) ->        decltype(void(std::declval().find(std::declval())),                                                         std::true_type{}); template auto test_find(long) -> std::false_type; template struct has_find : decltype(test_find(0)) {}; template< class T, class A0 > inline constexpr bool has_find_v = has_find::value; ``` 测试现在运行并通过。 6. 添加通用容器的实现;在这种情况下,向量。在**包含测试. cpp** 文件中写下以下测试: ```cpp TEST_F(containsTest, VectorContains) {     std::vector container {1,2,3,4,5};     ASSERT_TRUE(contains(container, 5));     ASSERT_FALSE(contains(container, 15)); } ``` 7. Add the basic implementation of `contains` to the **contains.hpp** file: ```cpp template auto contains(const C& c, const T& key) -> decltype(std::end(c), true) {         return std::end(c) != std::find(begin(c), end(c), key); } ``` 测试现在运行并通过。 8. 下一步是将`设置`特例的测试添加到**包含测试. cpp** : ```cpp TEST_F(containsTest, SetContains) {     std::set container {1,2,3,4,5};     ASSERT_TRUE(contains(container, 5));     ASSERT_FALSE(contains(container, 15)); } ``` 9. The implementation of `contains` is updated to test for the built-in `set::find()` method: ```cpp template auto contains(const C& c, const T& key) -> decltype(std::end(c), true) {     if constexpr(has_find_v)     {         return std::end(c) != c.find(key);     }     else     {         return std::end(c) != std::find(begin(c), end(c), key);     } } ``` 测试现在运行并通过。 10. 将`字符串`特例的测试添加到**包含测试. cpp** 文件: ```cpp TEST_F(containsTest, StringContains) {     std::string container{"This is the message"};     ASSERT_TRUE(contains(container, "the"));     ASSERT_TRUE(contains(container, 'm'));     ASSERT_FALSE(contains(container, "massage"));     ASSERT_FALSE(contains(container, 'z')); } ``` 11. Add the following implementation of `contains` to test for the presence of `npos` and tailor the use of the `find()` method: ```cpp template auto contains(const C& c, const T& key) -> decltype(std::end(c), true) {     if constexpr(has_npos_v)     {         return C::npos != c.find(key);     }     else     if constexpr(has_find_v)     {         return std::end(c) != c.find(key);     }     else     {         return std::end(c) != std::find(begin(c), end(c), key);     } } ``` 测试现在运行并通过。 12. 构建和运行名为`的应用包含`。创建新的运行配置。如果 contains 模板的实现是正确的,那么程序将显示以下输出: ![Figure 2B.36: Output from the successful implementation of contains ](img/C14583_02B_36.jpg) ###### 图 2B.36:成功实现包含的输出 在本练习中,我们结合 SFINAE 使用了各种模板技术,根据包含类的能力选择合适的`contains()`函数的实现。我们本可以使用一个通用的模板函数和一些专门的模板来达到同样的结果,但是我们选择了一条更少的道路,并展示了我们新发现的模板技能。 ## 第三章——能与应的距离——对象、指针和继承 ### 活动 1:用 RAII 和 Move 实现图形处理 在本练习中,我们将开发之前的`Matrix3d`和`Point3d`类,以使用`unique_ptr < >`来管理与实现这些图形类所需的数据结构相关联的内存。让我们开始吧: 1. 从**第 3 课/练习 01** 文件夹加载准备好的项目,并将项目的当前构建器配置为 **CMake Build(可移植)**。构建和配置启动器并运行单元测试。我们建议测试跑者的名字是 **L3A1graphicstests** 。 2. Open **point3d.hpp** and add the lines marked with a comment to the file: ```cpp // ... lines omitted #include #include namespace acpp::gfx { // Add this line class Point3d { // ... lines omitted }; } // Add this line ``` 请注意,添加到文件末尾的右大括号没有右分号。嵌套命名空间语法`acpp::gfx`,是 C++ 17 的一个新特性。以前,它需要两次明确使用`名称空间`关键字。另外,请注意,为了有所帮助,友好的邻居 IDE 可能会在您放入名称空间声明的行之后插入右大括号。 3. 对 **matrix3d.hpp** 、 **matrix3d.cpp** 和 **point3d.cpp** 重复相同的处理-确保包含文件不包含在命名空间的范围内。 4. 在相应的文件( **main.cpp** 、 **matrix3dTests.cpp** 和**point 3 tests . CPP**中,在完成#include 指令后,插入以下行: ```cpp using namespace acpp::gfx; ``` 5. 现在,运行所有测试。所有 **18** 现有测试应再次通过。我们已经成功地将我们的类放入了一个命名空间。 6. 现在我们将继续转换`Matrix3d`类来使用堆分配的内存。在 **matrix3d.hpp** 文件中,添加一个`#include <内存>`行,让我们访问`unique_ptr < >`模板。 7. 接下来,更改`m_data`的申报类型: ```cpp std::unique_ptr m_data; ``` 8. From this point forward, we will use the compiler and its errors to give us hints as to what needs fixing. Attempting to build the tests now reveals that we have a problem with the following two methods in the header file ```cpp float operator()(const int row, const int column) const {     return m_data[row][column]; } float& operator()(const int row, const int column) {     return m_data[row][column]; } ``` 这里的问题是`unique_ptr`持有一个指向一维数组而不是二维数组的指针。因此,我们需要将行和列转换成一个索引。 9. 添加一个名为`get_index()`的新方法,从行和列中获取一维索引,并更新前面的函数来使用它: ```cpp float operator()(const int row, const int column) const {     return m_data[get_index(row,column)]; } float& operator()(const int row, const int column) {     return m_data[get_index(row,column)]; } private: size_t get_index(const int row, const int column) const {     return row * NumberColumns + column; } ``` 10. 重新编译后,编译器的下一个错误引用了以下内联函数: ```cpp inline Matrix3d operator*(const Matrix3d& lhs, const Matrix3d& rhs) {     Matrix3d temp(lhs);   // <=== compiler error – ill formed copy constructor     temp *= rhs;     return temp; } ``` 11. Whereas before, the default copy constructor was sufficient for our purposes, it just did a shallow copy of all the elements of the array and that was correct. We now have indirection to the data we need to copy and so we need to implement a deep copy constructor and copy assignment. We will also need to address the existing constructors. For now, just add the constructor declarations to the class (adjacent to the other constructors): ```cpp Matrix3d(const Matrix3d& rhs); Matrix3d& operator=(const Matrix3d& rhs); ``` 尝试构建测试将会显示我们已经解决了头文件中的所有问题,并且我们可以进入实现文件。 12. 修改两个构造函数初始化`unique_ptr`如下: ```cpp Matrix3d::Matrix3d() : m_data{new float[NumberRows*NumberColumns]} {     for (int i{0} ; i< NumberRows ; i++)         for (int j{0} ; j< NumberColumns ; j++)             m_data[i][j] = (i==j); } Matrix3d::Matrix3d(std::initializer_list> list)     : m_data{new float[NumberRows*NumberColumns]} {     int i{0};     for(auto it1 = list.begin(); ibegin(); j> list)       : m_data{new float[NumberRows*NumberColumns]} {     int i{0};     for(auto it1 = list.begin(); ibegin(); j` to **matrix3d.cpp**, remove the comment from the output line in the move constructor. and re-run the test. It will report an error after the tests have completed because we sent it to the standard error channel (`cerr`). After the check, make the line a comment again. #### 注意 关于移动构造函数,只需简单说明一下——我们没有像对其他构造函数那样显式初始化`m_data`。这意味着它将被初始化为空,然后与传入的参数交换,这是一个临时参数,因此在事务后不保存数组是可以接受的——它删除了一次内存分配和释放。 24. 现在让我们转换`Point3d`类,以便它可以使用堆分配的内存。在 **point3d.hpp** 文件中,添加一个`#include <内存>`行,这样我们就可以访问`unique_ptr < >`模板。 25. 接下来,将`m_data`的声明类型更改为如下所示: ```cpp std::unique_ptr m_data; ``` 26. 编译器现在告诉我们插入操作符(< point3d.hpp 有问题,因为我们不能在 **unique_ptr** 上使用 ranged-for:用以下内容替换实现: ```cpp inline std::ostream& operator<<(std::ostream& os, const Point3d& pt) {     const char* sep = "[ ";     for(int i{0} ; i < Point3d::NumberRows ; i++)     {         os << sep << pt.m_data[i];         sep = ", ";     }     os << " ]";     return os; } ``` 27. 打开**点 3d.cpp** 并修改默认构造函数来初始化`unique_ptr`并更改初始化循环,因为在`unique_ptr` : ```cpp Point3d::Point3d() : m_data{new float[NumberRows]} {     for(int i{0} ; i < NumberRows-1 ; i++) {         m_data[i] = 0;     }     m_data[NumberRows-1] = 1; } ``` 上不能使用 ranged for 28. 通过初始化`唯一 _ptr` : ```cpp Point3d::Point3d(std::initializer_list list)             : m_data{new float[NumberRows]} ``` 来修改其他构造函数 29. 现在所有的测试都运行并通过了,就像以前一样。 30. 现在,如果我们运行原始应用 **L3graphics** ,那么输出将与原始应用相同,但是实现使用 RAII 来分配和管理用于矩阵和点的内存。 ![](img/C14583_03_52.jpg) ###### 图 3.52:成功转换为使用 RAII 后的活动 1 输出 ## 活动 2:实现日期计算的类 在这个活动中,我们将实现两个类,`日期`和`天数`,这将使我们很容易处理日期和它们之间的时间差。让我们开始吧: 1. 从**第 3 课/练习 02** 文件夹加载准备好的项目,并将项目的当前构建器配置为 **CMake Build(可移植)**。构建和配置启动器并运行单元测试。我们建议测试跑者的名字是 **L3A2datetests** 。该项目有虚拟文件和一个失败的测试。 2. 在编辑器中打开 **date.hpp** 文件,并在基本`Date`类中添加以下行,以允许访问存储的值: ```cpp int Day()   const {return m_day;} int Month() const {return m_month;} int Year()  const {return m_year;} ``` 3. Open the **dateTests.cpp** file and add the following code to the `DateTest` class: ```cpp void VerifyDate(const Date& dt, int yearExp, int monthExp, int dayExp) const {     ASSERT_EQ(dayExp, dt.Day());     ASSERT_EQ(monthExp, dt.Month());     ASSERT_EQ(yearExp, dt.Year()); } ``` 通常,随着测试的发展,您会重构这个测试,但是我们会提前把它拿出来。 4. 将现有测试中的`ASSERT_FALSE()`替换为以下测试: ```cpp Date dt; VerifyDate(dt, 1970, 1, 1); ``` 5. 重建并运行测试——它们现在应该都通过了。 6. 增加以下测试: ```cpp TEST_F(DateTest, Constructor1970Jan2) {     Date dt(2, 1, 1970);     VerifyDate(dt, 1970, 1, 2); } ``` 7. 为了进行这个测试,我们需要在`日期`类中添加以下两个构造函数: ```cpp Date() = default; Date(int day, int month, int year) :         m_year{year}, m_month{month}, m_day{day} { } ``` 8. 我们现在需要引入转换为/从`日期 _t`类型的函数。将以下别名添加到我们名称空间内的 **date.hpp** 文件中: ```cpp using date_t=int64_t; ``` 9. 在`日期`类中,添加以下方法的声明: ```cpp date_t ToDateT() const; ``` 10. 然后,添加以下测试: ```cpp TEST_F(DateTest, ToDateTDefaultIsZero) {     Date dt;     ASSERT_EQ(0, dt.ToDateT()); } ``` 11. 当我们在做(`TDD`)时,我们添加了方法的最小实现来通过测试。 ```cpp date_t Date::ToDateT() const {     return 0; } ``` 12. 现在,我们添加下一个测试: ```cpp TEST_F(DateTest, ToDateT1970Jan2Is1) {     Date dt(2, 1, 1970);     ASSERT_EQ(1, dt.ToDateT()); } ``` 13. 我们继续添加一个又一个测试,一直在`today te()`中细化算法,先处理`1970`中的日期,然后`1-1971 年 1 月`中的日期,然后是`1973`中的日期,这意味着我们跨越了一个闰年,以此类推。用于开发`TodayTet()`方法的全套测试如下: ```cpp TEST_F(DateTest, ToDateT1970Dec31Is364) {     Date dt(31, 12, 1970);     ASSERT_EQ(364, dt.ToDateT()); } TEST_F(DateTest, ToDateT1971Jan1Is365) {     Date dt(1, 1, 1971);     ASSERT_EQ(365, dt.ToDateT()); } TEST_F(DateTest, ToDateT1973Jan1Is1096) {     Date dt(1, 1, 1973);     ASSERT_EQ(365*3+1, dt.ToDateT()); } TEST_F(DateTest, ToDateT2019Aug28Is18136) {     Date dt(28, 8, 2019);     ASSERT_EQ(18136, dt.ToDateT()); } ``` 14. 为了通过所有这些测试,我们在`日期`类别的申报中添加了以下项目: ```cpp public:     static constexpr int EpochYear = 1970;     static constexpr int DaysPerCommonYear = 365;     static constexpr int YearsBetweenLeapYears = 4; private:     int GetDayOfYear(int day, int month, int year) const;     bool IsLeapYear(int year) const;     int CalcNumberLeapYearsFromEpoch(int year) const; ``` 15. `today Tet()`**date . CPP**的实施及配套方式如下: ```cpp namespace { int daysBeforeMonth[2][12] = {     { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 204, 334}, // Common Year     { 0, 31, 50, 91, 121, 152, 182, 213, 244, 274, 205, 335}  // Leap Year }; } namespace acpp::date { int Date::CalcNumberLeapYearsFromEpoch(int year) const {     return (year-1)/YearsBetweenLeapYears                                    - (EpochYear-1)/YearsBetweenLeapYears; } int Date::GetDayOfYear(int day, int month, int year) const {     return daysBeforeMonth[IsLeapYear(year)][month-1] + day; } bool Date::IsLeapYear(int year) const {     return (year%4)==0;   // Not full story, but good enough to 2100 } date_t Date::ToDateT() const {     date_t value = GetDayOfYear(m_day, m_month, m_year) - 1;     value += (m_year-EpochYear) * DaysPerCommonYear;     date_t numberLeapYears = CalcNumberLeapYearsFromEpoch(m_year);     value += numberLeapYears;     return value; } } ``` 16. 现在`todayt()`工作了,我们转到它的逆,也就是`FromDateT()`。同样,我们一次建立一个测试来开发一系列日期的算法。使用了以下测试: ```cpp TEST_F(DateTest, FromDateT0Is1Jan1970) {     Date dt;     dt.FromDateT(0);     ASSERT_EQ(0, dt.ToDateT());     VerifyDate(dt, 1970, 1, 1); } TEST_F(DateTest, FromDateT1Is2Jan1970) {     Date dt;     dt.FromDateT(1);     ASSERT_EQ(1, dt.ToDateT());     VerifyDate(dt, 1970, 1, 2); } TEST_F(DateTest, FromDateT364Is31Dec1970) {     Date dt;     dt.FromDateT(364);     ASSERT_EQ(364, dt.ToDateT());     VerifyDate(dt, 1970, 12, 31); } TEST_F(DateTest, FromDateT365Is1Jan1971) {     Date dt;     dt.FromDateT(365);     ASSERT_EQ(365, dt.ToDateT());     VerifyDate(dt, 1971, 1, 1); } TEST_F(DateTest, FromDateT1096Is1Jan1973) {     Date dt;     dt.FromDateT(1096);     ASSERT_EQ(1096, dt.ToDateT());     VerifyDate(dt, 1973, 1, 1); } TEST_F(DateTest, FromDateT18136Is28Aug2019) {     Date dt;     dt.FromDateT(18136);     ASSERT_EQ(18136, dt.ToDateT());     VerifyDate(dt, 2019, 8, 28); } ``` 17. 在头文件中添加以下声明: ```cpp public:     void FromDateT(date_t date); private:     int CalcMonthDayOfYearIsIn(int dayOfYear, bool IsLeapYear) const; ``` 18. 使用以下实现,因为前面的测试一次添加一个: ```cpp void Date::FromDateT(date_t date) {     int number_years = date / DaysPerCommonYear;     date = date - number_years * DaysPerCommonYear;     m_year = EpochYear + number_years;     date_t numberLeapYears = CalcNumberLeapYearsFromEpoch(m_year);     date -= numberLeapYears;     m_month = CalcMonthDayOfYearIsIn(date, IsLeapYear(m_year));     date -= daysBeforeMonth[IsLeapYear(m_year)][m_month-1];     m_day = date + 1; } int Date::CalcMonthDayOfYearIsIn(int dayOfYear, bool isLeapYear) const {     for(int i = 1 ; i < 12; i++)     {     if ( daysBeforeMonth[isLeapYear][i] > dayOfYear)             return i;     }     return 12; } ``` 19. 现在我们已经准备好了支持例程,我们可以实现两个日期之间的`日期`类差异的真正特征,并通过添加天数来确定新的日期。这两个操作都需要一个新的类型(类)`天`。 20. 在表头`日期`上方增加`天数`如下执行: ```cpp class Days { public:     Days() = default;     Days(int days) : m_days{days}     {    }     operator int() const     {         return m_days;     } private:     int m_days{0}; }; ``` 21. 第一个操作符是将`日`添加到`日`。添加以下方法声明(在`日期`类的公共部分内): ```cpp Date& operator+=(const Days& day); ``` 22. 然后,将内联实现(在`日期`类之外)添加到头文件: ```cpp inline Date operator+(const Date& lhs, const Days& rhs ) {     Date tmp(lhs);     tmp += rhs;     return tmp; } ``` 23. 编写以下测试以验证`总和`操作: ```cpp TEST_F(DateTest, AddZeroDays) {     Date dt(28, 8, 2019);     Days days;     dt += days;     VerifyDate(dt, 2019, 8, 28); } TEST_F(DateTest, AddFourDays) {     Date dt(28, 8, 2019);     Days days(4);     dt += days;     VerifyDate(dt, 2019, 9, 1); } ``` 24. `sum`操作的实际实现简单基于两种支持方式 ```cpp Date& Date::operator+=(const Days& day) {     FromDateT(ToDateT()+day);     return *this; } ``` 25. 增加以下测试: ```cpp TEST_F(DateTest, AddFourDaysAsInt) {     Date dt(28, 8, 2019);     dt += 4;     VerifyDate(dt, 2019, 9, 1); } ``` 26. 当我们运行测试时,它们都构建好了,这个测试通过了。但这不是理想的结果。我们不希望他们能够在我们的日期上加上整数。(未来的版本可能会增加月份和年份,那么增加一个整数意味着什么呢?).为了避免构建失败,我们将 Days 构造函数改为`显式` : ```cpp explicit Days(int days) : m_days{days}     {    } ``` 27. Now the build fails, so we need to fix the test by changing the addition line to cast to `Days` as follows: ```cpp dt += static_cast(4); ``` 所有测试都应该再次通过。 28. 我们想要的最后一个功能是两个日期之间的差异。以下是用于验证实现的测试: ```cpp TEST_F(DateTest, DateDifferences27days) {     Date dt1(28, 8, 2019);     Date dt2(1, 8, 2019);     Days days = dt1 - dt2;     ASSERT_EQ(27, (int)days); } TEST_F(DateTest, DateDifferences365days) {     Date dt1(28, 8, 2019);     Date dt2(28, 8, 2018);     Days days = dt1 - dt2;     ASSERT_EQ(365, (int)days); } ``` 29. 在头文件`日期`类的公共部分添加以下函数声明: ```cpp Days operator-(const Date& rhs) const; ``` 30. Add the following code after the Date class in the header file: ```cpp inline Days Date::operator-(const Date& rhs) const {     return Days(ToDateT() - rhs.ToDateT()); } ``` 因为我们将`Days`构造函数显式化了,所以我们必须在 return 语句中调用它。随着所有这些变化的到位,所有的测试都应该通过。 31. 将`L3A2date`配置为`datetools`二进制,在编辑器中打开 main.cpp。从`活动 2`的定义中删除注释: ```cpp #define ACTIVITY2 ``` 32. 构建并运行示例应用。这将产生以下输出: ![Figure 3.53: Output of successful Date sample application ](img/C14583_03_53.jpg) ###### 图 3.53:成功的日期示例应用的输出 我们已经实现了日期和天数类的所有要求,并通过单元测试交付了它们。单元测试允许我们实现增量功能来构建两个复杂的算法,即`todayt`和`FromDateT`,它们形成了对我们想要交付的功能的底层支持。 ## 第 4 章-关注点分离-软件架构、功能、可变模板 ### 活动 1:实现多播事件处理程序 1. 从**第 4 课/练习 01** 文件夹加载准备好的项目,并将项目的当前构建器配置为可移植构建。构建项目,配置启动器并运行单元测试(未通过一个虚拟测试)。推荐测试运行者的名字为 *L4delegateTests* 。 2. 在**委托测试. cpp** 中,用以下测试替换失败的虚拟测试: ```cpp TEST_F(DelegateTest, BasicDelegate) {     Delegate delegate;     ASSERT_NO_THROW(delegate.Notify(42)); } ``` 3. This now fails to build, so we need to add a new method to `Delegate`. As this will evolve into a template, we will do all of this development in the header file. In **delegate.hpp**, and the following definition: ```cpp class Delegate { public:     Delegate() = default;     void Notify(int value) const     {     } }; ``` 测试现在运行并通过。 4. 在现有测试中添加以下行: ```cpp ASSERT_NO_THROW(delegate(22)); ``` 5. Again, the build fails, so we update the `Delegate` definition as follows (we could have had Notify call `operator()`, but this is easier to read): ```cpp void operator()(int value) {     Notify(value); } ``` 测试再次运行并通过。 6. 在添加下一个测试之前,我们将添加一些基础设施来帮助我们开发测试。处理程序最简单的方法是让它们写入`std::cout`,为了能够验证它们被调用,我们需要捕获输出。为此,通过更改`DelegateTest`类将标准输出流重新路由到不同的缓冲区,如下所示: ```cpp class DelegateTest : public ::testing::Test { public:     void SetUp() override;     void TearDown() override;     std::stringstream m_buffer;     // Save cout's buffer here     std::streambuf *m_savedBuf{}; }; void DelegateTest::SetUp() {     // Save the cout buffer     m_savedBuf = std::cout.rdbuf();     // Redirect cout to our buffer     std::cout.rdbuf(m_buffer.rdbuf()); } void DelegateTest::TearDown() {     // Restore cout buffer to original     std::cout.rdbuf(m_savedBuf); } ``` 7. 同时将`<>``<>`和`<字符串>`的 include 语句添加到文件顶部。 8. 有了这个支持框架,添加以下测试: ```cpp TEST_F(DelegateTest, SingleCallback) {     Delegate delegate;     delegate += [] (int value) { std::cout << "value = " << value; };     delegate.Notify(42);     std::string result = m_buffer.str();     ASSERT_STREQ("value = 42", result.c_str()); } ``` 9. To make the tests build and run again, add the following code in the **delegate.h** class: ```cpp Delegate& operator+=(const std::function& delegate) {     m_delegate = delegate;     return *this; } ``` 以及以下代码: ```cpp private:     std::function m_delegate; ``` 测试正在构建,但是我们的新测试失败了。 10. 将`通知()`方法更新为: ```cpp void Notify(int value) const {     m_delegate(value); } ``` 11. The tests now build and our new test passes, but the original test now fails. The call to the delegate is throwing an exception, so we need to check that the delegate is not empty before calling it. Write the following code to do this: ```cpp void Notify(int value) const {     if(m_delegate)         m_delegate(value); } ``` 所有的测试现在运行并通过。 12. 我们现在需要向`代理`类添加多播支持。添加新测试: ```cpp TEST_F(DelegateTest, DualCallbacks) {     Delegate delegate;     delegate += [] (int value) { std::cout << "1: = " << value << "\n"; };     delegate += [] (int value) { std::cout << "2: = " << value << "\n"; };     delegate.Notify(12);     std::string result = m_buffer.str();     ASSERT_STREQ("1: = 12\n2: = 12\n", result.c_str()); } ``` 13. Of course, this test now fails because the `operator+=()` only assigns to the member variable. We need to add a list to store our delegates. We choose vector so we can add to the end of the list as we want to call the delegates in the order that they are added. Add `#include ` to the top of **delegate.hpp** and update Delegate replace **m_delegate** with **m_delegates** vector of the callbacks: ```cpp class Delegate { public:     Delegate() = default;     Delegate& operator+=(const std::function& delegate)     {         m_delegates.push_back(delegate);         return *this;     }     void Notify(int value) const     {         for(auto& delegate : m_delegates)         {             delegate(value);         }     }     void operator()(int value)     {         Notify(value);     } private:     std::vector> m_delegates; }; ``` 测试全部运行并再次通过。 14. 我们现在已经实现了基本的多播`委托`类。我们现在需要将其转换为基于模板的类。通过将三个测试中`委托`的所有声明更改为`委托< int >`,更新现有测试。 15. 现在更新 Delegate 类,在类前添加`模板<类 Arg >`将其转换为模板,并用`Arg` : ```cpp template class Delegate { public:     Delegate() = default;     Delegate& operator+=(const std::function& delegate)     {         m_delegates.push_back(delegate);         return *this;     }     void Notify(Arg value) const     {         for(auto& delegate : m_delegates)         {             delegate(value);         }     }     void operator()(Arg value)     {         Notify(value);     } private:     std::vector> m_delegates; }; ``` 替换四次出现的`int` 16. 所有测试现在都像以前一样运行并通过,所以它仍然适用于处理程序的`int`参数。 17. 添加以下测试并重新运行测试,以确认模板转换正确: ```cpp TEST_F(DelegateTest, DualCallbacksString) {     Delegate delegate;     delegate += [] (std::string value) { std::cout << "1: = " << value << "\n"; };     delegate += [] (std::string value) { std::cout << "2: = " << value << "\n"; };     std::string hi{"hi"};     delegate.Notify(hi);     std::string result = m_buffer.str();     ASSERT_STREQ("1: = hi\n2: = hi\n", result.c_str()); } ``` 18. Now it operates as a template that takes one argument. We need to convert it into a variadic template that takes zero or more arguments. Using the information from the last topic, update the template to the following: ```cpp template class Delegate { public:     Delegate() = default;     Delegate& operator+=(const std::function& delegate)     {         m_delegates.push_back(delegate);         return *this;     }     void Notify(ArgTypes&&... args) const     {         for(auto& delegate : m_delegates)         {             delegate(std::forward(args)...);         }     }     void operator()(ArgTypes&&... args)     {         Notify(std::forward(args)...);     } private:     std::vector> m_delegates; }; ``` 测试应该仍然运行并通过。 19. Add two more tests – zero argument test, and a mutliple argument test: ```cpp TEST_F(DelegateTest, DualCallbacksNoArgs) {     Delegate delegate;     delegate += [] () { std::cout << "CB1\n"; };     delegate += [] () { std::cout << "CB2\n"; };     delegate.Notify();     std::string result = m_buffer.str();     ASSERT_STREQ("CB1\nCB2\n", result.c_str()); } TEST_F(DelegateTest, DualCallbacksStringAndInt) {     Delegate delegate;     delegate += [] (std::string& value, int i) {             std::cout << "1: = " << value << "," << i << "\n"; };     delegate += [] (std::string& value, int i) {         std::cout << "2: = " << value << "," << i << "\n"; };     std::string hi{"hi"};     delegate.Notify(hi, 52);     std::string result = m_buffer.str();     ASSERT_STREQ("1: = hi,52\n2: = hi,52\n", result.c_str()); } ``` 所有测试运行并通过,表明我们现在已经实现了期望的`委托`类。 20. Now, change the Run configuration to execute the program `L4delegate`. Open the **main.cpp** file in the editor and change the definition at the top of the file to the following and run the program: ```cpp #define ACTIVITY_STEP 27 ``` 我们得到以下输出: ![Figure 4.35: Output from the successful implementation of Delegate ](img/C14583_04_35.jpg) ###### 图 4.35:成功实现委托的输出 在本练习中,我们首先实现了一个提供基本单委托功能的类,然后添加了多播功能。有了这个实现,单元测试就位,我们能够快速地转换成一个带有一个参数的模板,然后转换成一个可变的模板版本。根据您正在开发的功能,特定实现过渡到通用形式,然后再过渡到更通用形式的方法是正确的。可变模板的开发并不总是显而易见的。 ## 第 5 章——哲学家的晚餐——线程和并发 ### 活动 1:创建一个模拟器来模拟美术馆的工作 美术馆工作模拟器是一个模拟参观者和看守人行为的应用。参观者有数量限制,即只能有 50 人同时进入画廊。参观者不断来到画廊。看守人检查是否超过了访客人数的限制。如果是这样,它会要求新访客等待,并将他们放在等候名单上。如果没有,它允许他们进入画廊。参观者可以随时离开画廊。如果有人离开画廊,看守人会让等候名单上的人进入画廊。 按照以下步骤实施本活动: 1. 创建一个文件,其中包含我们这个项目需要的所有常量。 2. 添加包括警卫和第一个变量,内的**人数,代表访客人数限制为 50 人: ```cpp #ifndef COMMON_HPP #define COMMON_HPP constexpr size_t CountPeopleInside = 5; #endif // COMMON_HPP ```** 3. 现在,为`Person`类创建一个头文件和源文件,即`Person.hpp`和`Person.cpp`。另外,添加包含防护装置。定义`人`类,删除复制构造函数和复制赋值运算符;我们将只使用用户定义的默认构造函数、移动构造函数、移动赋值运算符和默认析构函数。添加一个名为`m_Id`的私有变量;我们会用它来记录。另外,添加一个名为`m_NextId`的私有静态变量;它将用于生成唯一标识: ```cpp #ifndef PERSON_HPP #define PERSON_HPP class Person { public:     Person();     Person& operator=(Person&);     Person(Person&&);     ~Person() = default;     Person(const Person&) = delete;     Person& operator=(const Person&) = delete; private:     int m_Id;     static int m_NextId; }; #endif // PERSON_HPP ``` 4. 在源文件中,定义我们的静态变量,`m_NextId`。然后,在构造函数中,用`m_NextId`的值初始化`m_Id`变量。在构造函数中打印日志。实现移动复制构造函数和移动赋值运算符。现在,为我们的`人`对象实现线程安全存储。创建所需的头文件和源文件,即`Persons.hpp`和`Persons.cpp`。另外,添加包含防护装置。包括“`Person.hpp`”和`<互斥>`和`<向量>`头。使用用户定义的默认构造函数和默认析构函数定义`Persons`类。申报`添加()`功能添加人物,`获取()`获取人物并从列表中删除。定义`size()`函数获取 Person 元素的计数,以及`removePerson()`,从存储中移除任何人。在私有部分,声明一个互斥类型的变量,即`m_Mutex`,以及存储 Persons 的向量,即`m _ Persons`:T0 5. 在源文件中,声明用户定义的构造函数,其中我们将向量的大小保留为 50 个元素(以避免在增长过程中调整大小): ```cpp Persons::Persons() {     m_Persons.reserve(CountPeopleInside); } ``` 6. 声明`add()`函数,该函数采用`Person`类型的右值参数,锁定互斥体,并使用`std::move()`函数将`Person`添加到向量中: ```cpp void Persons::add(Person&& person) {     std::lock_guard m_lock(m_Mutex);     m_Persons.emplace_back(std::move(person)); } ``` 7. 声明`get()`函数,该函数锁定互斥体并返回最后一个元素,然后将其从向量中移除。如果向量为空,将抛出异常: ```cpp Person Persons::get() {     std::lock_guard m_lock(m_Mutex);     if (m_Persons.empty())     {         throw "Empty Persons storage";     }     Person result = std::move(m_Persons.back());     m_Persons.pop_back();     return result; } ``` 8. 声明`size()`函数,返回向量的大小: ```cpp size_t Persons::size() const {     return m_Persons.size(); } ``` 9. 最后,声明`removePerson()`函数,该函数锁定互斥体并从向量中移除最后一项: ```cpp void Persons::removePerson() {     std::lock_guard m_lock(m_Mutex);     m_Persons.pop_back();     std::cout << "Persons | removePerson | removed" << std::endl; } ``` 10. 现在,实现 PersonGenerator 类,它负责创建和删除 Person 项。创建各自的头文件和源文件,即`PersonGenerator.hpp`和`PersonGenerator.cpp`。另外,添加包含防护装置。包括“`Person.hpp`”、`<线程>`和`<条件变量>`头文件。定义`人员生成器`类。在私有部分,定义两个`std::thread`变量,即`m_CreateThread`和`m_RemoveThread`。在一个线程中,我们将创建新的`人物`对象,并将异步通知用户移除另一个线程中的`人物`对象。定义对`人员`类型的共享变量的引用,即`m_CreatedPersons`。我们会把每一个新人都放进去。`m_CreatedPersons`将在几个线程之间共享。定义对`STD::condition _ variable`的两个引用,即`m _ convaraddperson`和`m _ convarremoveperson`。它们将用于线程之间的通信。定义对`std::mutex`变量的两个引用,即`m_AddLock`和`m_RemoveLock`。它们将用于接收对条件变量的访问。最后,定义一个`bool`值的两个引用,即`m_AddNotified`和`m_RemoveNotified`。它们将用于检查通知是真是假。另外,在私有部分,定义两个函数,这两个函数将是我们线程的启动函数–`runCreating()`和`runremove()`。接下来,定义两个将触发条件变量的函数,即`notifyCreated()`和`notifyRemoved()`。在公共部分,定义一个构造函数,它将我们在私有部分定义的所有引用作为参数。最后,定义析构函数。这将确保删除其他默认生成的函数: ```cpp #ifndef PERSON_GENERATOR_HPP #define PERSON_GENERATOR_HPP #include "Persons.hpp" #include #include class PersonGenerator { public:     PersonGenerator(Persons& persons,             std::condition_variable& add_person,             std::condition_variable& remove_person,             std::mutex& add_lock,             std::mutex& remove_lock,             bool& addNotified,             bool& removeNotified);     ~PersonGenerator();     PersonGenerator(const PersonGenerator&) = delete;     PersonGenerator(PersonGenerator&&) = delete;     PersonGenerator& operator=(const PersonGenerator&) = delete;     PersonGenerator& operator=(PersonGenerator&&) = delete; private:     void runCreating();     void runRemoving();     void notifyCreated();     void notifyRemoved(); private:     std::thread m_CreateThread;     std::thread m_RemoveThread;     Persons& m_CreatedPersons;     // to notify about creating new person     std::condition_variable& m_CondVarAddPerson;     std::mutex& m_AddLock;     bool& m_AddNotified;     // to notify that person needs to be removed     std::condition_variable& m_CondVarRemovePerson;     std::mutex& m_RemoveLock;     bool& m_RemoveNotified; }; #endif // PERSON_GENERATOR_HPP ``` 11. 现在,转到源文件。包括`< stdlib.h >`文件,以便我们可以访问`srand()`和`rand()`函数,这些函数用于随机数生成。包括`< time.h >`头,这样我们就可以访问`time()`功能,以及`std::chrono`名称空间。它们用于我们与时间打交道的时候。包括`<比例>`文件,用于 typedefs,以便我们可以使用时间库: ```cpp #include "PersonGenerator.hpp" #include #include      /* srand, rand */ #include        /* time, chrono */ #include         /* std::milli */ ``` 12. 声明构造函数并初始化除初始化列表中的线程之外的所有参数。用构造函数体中的适当函数初始化线程: ```cpp PersonGenerator::PersonGenerator(Persons& persons,                     std::condition_variable& add_person,                     std::condition_variable& remove_person,                     std::mutex& add_lock,                     std::mutex& remove_lock,                     bool& addNotified,                     bool& removeNotified)     : m_CreatedPersons(persons)     , m_CondVarAddPerson(add_person)     , m_AddLock(add_lock)     , m_AddNotified(addNotified)     , m_CondVarRemovePerson(remove_person)     , m_RemoveLock(remove_lock)     , m_RemoveNotified(removeNotified) {     m_CreateThread = std::thread(&PersonGenerator::runCreating, this);     m_RemoveThread = std::thread(&PersonGenerator::runRemoving, this); } ``` 13. 声明一个析构函数并检查线程是否可连接。如果不是,加入他们: ```cpp PersonGenerator::~PersonGenerator() {     if (m_CreateThread.joinable())     {         m_CreateThread.join();     }     if (m_RemoveThread.joinable())     {         m_RemoveThread.join();     } } ``` 14. 声明`runCreating()`函数,这是`m_CreateThread`线程的启动函数。在这个函数中,在一个无限循环中,我们将生成一个从 1 到 10 的随机数,并使当前线程在这段时间休眠。之后,创建一个 Person 值,添加到共享容器中,并通知其他线程: ```cpp void PersonGenerator::runCreating() {     using namespace std::chrono_literals;     srand (time(NULL));     while(true)     {         std::chrono::duration duration((rand() % 10 + 1)*1000);         std::this_thread::sleep_for(duration);         std::cout << "PersonGenerator | runCreating | new person:" << std::endl;         m_CreatedPersons.add(std::move(Person()));         notifyCreated();     } } ``` 15. 声明`运行删除()`功能,这是`m_RemoveThread`线程的启动功能。在这个函数中,在一个无限循环中,我们将生成一个从 20 到 30 的随机数,并使当前线程在这段时间休眠。之后,通知其他线程应该移除部分访客: ```cpp void PersonGenerator::runRemoving() {     using namespace std::chrono_literals;     srand (time(NULL));     while(true)     {         std::chrono::duration duration((rand() % 10 + 20)*1000);         std::this_thread::sleep_for(duration);         std::cout << "PersonGenerator | runRemoving | somebody has left the gallery:" << std::endl;         notifyRemoved();     } } ``` 16. 声明`通知创建()`和`通知删除()`功能。在它们的体内,锁定适当的互斥体,将适当的 bool 变量设置为 true,并在适当的条件变量上调用`notify_all()`函数: ```cpp void PersonGenerator::notifyCreated() {     std::unique_lock lock(m_AddLock);     m_AddNotified = true;     m_CondVarAddPerson.notify_all(); } void PersonGenerator::notifyRemoved() {     std::unique_lock lock(m_RemoveLock);     m_RemoveNotified = true;     m_CondVarRemovePerson.notify_all(); } ``` 17. 最后,我们需要为最后一个类“守望者”创建文件,即`守望者. hpp`和`守望者. cpp`。像往常一样,添加包括警卫。包括“`Persons.hpp`”、`<线程>`、<互斥体>和`<条件变量>`头。定义`守望者`类。在私有部分,定义两个`std::thread`变量,即`m_ThreadAdd`和`m_ThreadRemove`。在其中一个线程中,我们会将新的`人物`对象移动到适当的队列中,并异步移除另一个线程中的`人物`对象。定义对共享`人员`变量的引用,即`m_CreatedPeople`、`m_PeopleInside`、`m_PeopleInQueue`。如果没有超过限制,我们将从`m_CreatedPeople`列表中取出每个新人员,并将其移动到`m_PeopleInside`列表中。否则,我们将把它们移到`m_PeopleInQueue`列表中。它们将在几个线程之间共享。定义对`标准::条件变量`的两个引用,即`m _ convaraddperson`和`m _ convarremoveperson`。它们将用于线程之间的通信。定义对`std::mutex`变量的两个引用,即`m_AddMux`和`m_RemoveMux`。它们将用于接收对条件变量的访问。最后,定义一个`bool`值的两个引用,即`m_AddNotified`和`m_RemoveNotified`。它们将用于检查通知是真是假。另外,在私有部分,定义两个函数,这两个函数将是我们线程的启动函数–`runAdd()`和`runRemove()`。在公共部分,定义一个构造函数,它将我们在私有部分定义的所有引用作为参数。现在,定义一个析构函数。确保删除所有其他默认生成的函数: ```cpp #ifndef WATCHMAN_HPP #define WATCHMAN_HPP #include #include #include #include "Persons.hpp" class Watchman { public:     Watchman(std::condition_variable&,             std::condition_variable&,             std::mutex&,             std::mutex&,             bool&,             bool&,             Persons&,             Persons&,             Persons&);     ~Watchman();     Watchman(const Watchman&) = delete;     Watchman(Watchman&&) = delete;     Watchman& operator=(const Watchman&) = delete;     Watchman& operator=(Watchman&&) = delete; private:     void runAdd();     void runRemove(); private:     std::thread m_ThreadAdd;     std::thread m_ThreadRemove;     std::condition_variable& m_CondVarRemovePerson;     std::condition_variable& m_CondVarAddPerson;     std::mutex& m_AddMux;     std::mutex& m_RemoveMux;     bool& m_AddNotified;     bool& m_RemoveNotified;     Persons& m_PeopleInside;     Persons& m_PeopleInQueue;     Persons& m_CreatedPeople; }; #endif // WATCHMAN_HPP ``` 18. 现在,转到源文件。包括“ **Common.hpp** ”头,以便我们可以访问变量内的**m _ counterpeople 和其他必要的头: ```cpp #include "Watchman.hpp" #include "Common.hpp" #include ```** 19. 声明构造函数并初始化除初始化列表中的线程之外的所有参数。用构造器主体中的适当函数初始化线程: ```cpp Watchman::Watchman(std::condition_variable& addPerson,             std::condition_variable& removePerson,             std::mutex& addMux,             std::mutex& removeMux,             bool& addNotified,             bool& removeNotified,             Persons& peopleInside,             Persons& peopleInQueue,             Persons& createdPeople)     : m_CondVarRemovePerson(removePerson)     , m_CondVarAddPerson(addPerson)     , m_AddMux(addMux)     , m_RemoveMux(removeMux)     , m_AddNotified(addNotified)     , m_RemoveNotified(removeNotified)     , m_PeopleInside(peopleInside)     , m_PeopleInQueue(peopleInQueue)     , m_CreatedPeople(createdPeople) {     m_ThreadAdd = std::thread(&Watchman::runAdd, this);     m_ThreadRemove = std::thread(&Watchman::runRemove, this); } ``` 20. 声明一个析构函数并检查线程是否可连接。如果不是,加入他们: ```cpp Watchman::~Watchman() {     if (m_ThreadAdd.joinable())     {         m_ThreadAdd.join();     }     if (m_ThreadRemove.joinable())     {         m_ThreadRemove.join();     } } ``` 21. 声明`runAdd()`功能。在这里,我们创建一个无限循环。在循环中,我们在等待一个条件变量。当条件变量通知时,我们从`m_CreatedPeople`列表中选取人员,并将其移动到适当的列表中,即`m_PeopleInside`或`m_PeopleInQueue`(如果已超过限制)。然后,我们检查`m_PeopleInQueue`列表中是否有人员,如果`m _ peoplein`未满,我们将他们移入此列表: ```cpp void Watchman::runAdd() {     while (true)     {         std::unique_lock locker(m_AddMux);         while(!m_AddNotified)         {             std::cerr << "Watchman | runAdd | false awakening" << std::endl;             m_CondVarAddPerson.wait(locker);         }         std::cout << "Watchman | runAdd | new person came" << std::endl;         m_AddNotified = false;         while (m_CreatedPeople.size() > 0)         {             try             {                 auto person = m_CreatedPeople.get();                 if (m_PeopleInside.size() < CountPeopleInside)                 {                     std::cout << "Watchman | runAdd | welcome in our The Art Gallery" << std::endl;                     m_PeopleInside.add(std::move(person));                 }                 else                 {                     std::cout << "Watchman | runAdd | Sorry, we are full. Please wait" << std::endl;                     m_PeopleInQueue.add(std::move(person));                 }             }             catch(const std::string& e)             {                 std::cout << e << std::endl;             }         }         std::cout << "Watchman | runAdd | check people in queue" << std::endl;         if (m_PeopleInQueue.size() > 0)         {             while (m_PeopleInside.size() < CountPeopleInside)             {                 try                 {                     auto person = m_PeopleInQueue.get();                     std::cout << "Watchman | runAdd | welcome in our The Art Gallery" << std::endl;                     m_PeopleInside.add(std::move(person));                 }                 catch(const std::string& e)                 {                     std::cout << e << std::endl;                 }             }         }     } } ``` 22. 接下来,声明`runRemove()`功能,我们将从`m_PeopleInside`中移除访问者。在这里,同样在无限循环中,我们在等待`m _ convarremoveperson`条件变量。当它通知线程时,我们从访问者列表中删除人员。接下来,我们将检查`m_PeopleInQueue`列表中是否有任何人,如果没有超过限制,我们将他们添加到`m _ PeopleInQueue`中: ```cpp void Watchman::runRemove() {     while (true)     {         std::unique_lock locker(m_RemoveMux);         while(!m_RemoveNotified)         {             std::cerr << "Watchman | runRemove | false awakening" << std::endl;             m_CondVarRemovePerson.wait(locker);         }         m_RemoveNotified = false;         if (m_PeopleInside.size() > 0)         {             m_PeopleInside.removePerson();             std::cout << "Watchman | runRemove | good buy" << std::endl;         }         else         {             std::cout << "Watchman | runRemove | there is nobody in The Art Gallery" << std::endl;         }         std::cout << "Watchman | runRemove | check people in queue" << std::endl;         if (m_PeopleInQueue.size() > 0)         {             while (m_PeopleInside.size() < CountPeopleInside)             {                 try                 {                     auto person = m_PeopleInQueue.get();                     std::cout << "Watchman | runRemove | welcome in our The Art Gallery" << std::endl;                     m_PeopleInside.add(std::move(person));                 }                 catch(const std::string& e)                 {                     std::cout << e << std::endl;                 }             }         }     } } ``` 23. 最后,转到`主()`功能。首先,创建我们在`守望者`和`人员生成器`类中使用的所有共享变量。接下来,创建`watcher`和`PersonGenerator`变量,并将这些共享变量传递给构造函数。在主函数的末尾读取字符以避免关闭应用: ```cpp int main() {     {         std::condition_variable g_CondVarRemovePerson;         std::condition_variable g_CondVarAddPerson;         std::mutex g_AddMux;         std::mutex g_RemoveMux;         bool g_AddNotified = false;;         bool g_RemoveNotified = false;         Persons g_PeopleInside;         Persons g_PeopleInQueue;         Persons g_CreatedPersons;         PersonGenerator generator(g_CreatedPersons, g_CondVarAddPerson, g_CondVarRemovePerson,                         g_AddMux, g_RemoveMux, g_AddNotified, g_RemoveNotified);         Watchman watchman(g_CondVarAddPerson,                 g_CondVarRemovePerson,                 g_AddMux,                 g_RemoveMux,                 g_AddNotified,                 g_RemoveNotified,                 g_PeopleInside,                 g_PeopleInQueue,                 g_CreatedPersons);     }     char a;     std::cin >> a;     return 0; } ``` 24. 编译并运行应用。在终端中,您将看到来自不同线程的日志,这些日志是关于创建人员以及将人员从一个列表移动到另一个列表。您的输出将类似于下面的截图: ![Figure 5.27: The result of the application's execution ](img/C14583_05_27.jpg) ###### 图 5.27:应用执行的结果 如您所见,所有线程都以非常简单和干净的方式相互通信。我们通过使用互斥来保护我们的共享数据,这样我们就可以避免竞争条件。在这里,我们使用了一个异常来警告空列表,并在线程的函数中捕获它们,这样我们的线程就可以自己处理异常。在将线程加入析构函数之前,我们还检查了它是否是可连接的。这使我们避免了计划的意外终止。因此,这个小项目展示了我们处理线程的技巧。 ## 第 6 章–流和输入/输出 ### 活动 1 美术馆模拟器的记录系统 线程安全记录器允许我们同时向终端输出数据。我们通过继承`std::ostringstream`类并使用互斥体进行同步来实现这个记录器。我们将实现一个为格式化输出提供接口的类,我们的记录器将使用它来扩展基本输出。我们为不同的日志记录级别定义了宏定义,以提供一个简单明了的界面。按照以下步骤完成本活动: 1. 从第 6 课打开项目。 2. Create a new directory called logger inside the **src/** directory. You will get the following hierarchy: ![Figure 6.25: The hierarchy of the project ](img/C14583_06_25.jpg) ###### 图 6.25:项目的层次结构 3. 创建一个名为**记录器**的头文件和源文件。在**中,添加包括守卫。包括<字符串>头,以增加对使用字符串的支持。定义一个名为 logger 的命名空间,然后定义一个名为 **utils** 的嵌套命名空间。在 **utils** 命名空间中,声明 **LoggerUtils** 类。** 4. 在公共部分 n 中,声明以下静态函数:`getDateTime`、`getThreadId`、`getLoggingLevel`、`getFileAndLine`、`getFuncName`、`getinfouncname`、`getOutFuncName`。你的班级应该如下所示: ```cpp #ifndef LOGGERUTILS_HPP_ #define LOGGERUTILS_HPP_ #include namespace logger { namespace utils { class LoggerUtils { public:      static std::string getDateTime();      static std::string getThreadId();      static std::string getLoggingLevel(const std::string& level);      static std::string getFileAndLine(const std::string& file, const int& line);      static std::string getFuncName(const std::string& func);      static std::string getInFuncName(const std::string& func);      static std::string getOutFuncName(const std::string& func); }; } // namespace utils } // namespace logger #endif /* LOGGERUTILS_HPP_ */ ``` 5. 在 **LoggerUtils.cpp** 中,增加需要的包括: **LoggerUtils.hpp** 表头、**T11】stream>T6】为 **std::stringstream** 支持、**T13】ctime>T10】为日期时间支持: ```cpp #include "LoggerUtils.hpp" #include #include #include ```**** 6. 进入`记录器`和`实用程序`名称空间。编写所需的函数定义。在`getDateTime()`功能中,使用`localtime()`功能获取当地时间。使用`str time()`函数将其格式化为字符串。使用`标准::字符串流` : ```cpp std::string LoggerUtils::getDateTime() {      time_t rawtime;      struct tm * timeinfo;      char buffer[80];      time (&rawtime);      timeinfo = localtime(&rawtime);      strftime(buffer,sizeof(buffer),"%d-%m-%YT%H:%M:%S",timeinfo);      std::stringstream ss;      ss << "[";      ss << buffer;      ss << "]";      return ss.str(); } ``` 将其转换为所需的格式 7. 在`getThreadId()`函数中,获取当前线程 Id,并使用`std::stringstream` : ```cpp std::string LoggerUtils::getThreadId() {      std::stringstream ss;      ss << "[";      ss << std::this_thread::get_id();      ss << "]";      return ss.str(); } ``` 将其转换为所需格式 8. 在`getLoggingLevel()`函数中,使用`std::stringstream` : ```cpp std::string LoggerUtils::getLoggingLevel(const std::string& level) {      std::stringstream ss;      ss << "[";      ss << level;      ss << "]";      return ss.str(); } ``` 将给定字符串转换为所需格式 9. 在`getfilandline()`函数中,使用`std::stringstream` : ```cpp std::string LoggerUtils::getFileAndLine(const std::string& file, const int& line) {      std::stringstream ss;      ss << " ";      ss << file;      ss << ":";      ss << line;      ss << ":";      return ss.str(); } ``` 将给定的文件和行转换为所需的格式 10. 在`getFuncName()`函数中,使用`std::stringstream` : ```cpp std::string LoggerUtils::getFuncName(const std::string& func) {      std::stringstream ss;      ss << " --- ";      ss << func;      ss << "()";      return ss.str(); } ``` 将函数名转换为所需的格式 11. 在`getInFuncName()`函数中,使用`std::stringstream`将函数名转换为所需的格式。 ```cpp std::string LoggerUtils::getInFuncName(const std::string& func) {      std::stringstream ss;      ss << " --> ";      ss << func;      ss << "()";      return ss.str(); } ``` 12. 在`getOutFuncName()`函数中,使用`std::stringstream` : ```cpp std::string LoggerUtils::getOutFuncName(const std::string& func) {      std::stringstream ss;      ss << " <-- ";      ss << func;      ss << "()";      return ss.str(); } ``` 将函数名转换为所需的格式 13. 创建一个名为**的头文件**。添加包括防护装置。为每个 **LoggerUtils** 函数创建宏定义: **DATETIME** 为 **getDateTime()** 函数, **THREAD_ID** 为 **getThreadId()** 函数, **LOG_LEVEL** 为 **getLoggingLevel()** 函数, **FILE_LINE** 为**getfileadline()**函数, **FUNC 因此,头文件应该如下所示: ```cpp #ifndef LOGGERMACROSES_HPP_ #define LOGGERMACROSES_HPP_ #define DATETIME \      logger::utils::LoggerUtils::getDateTime() #define THREAD_ID \      logger::utils::LoggerUtils::getThreadId() #define LOG_LEVEL( level ) \      logger::utils::LoggerUtils::getLoggingLevel(level) #define FILE_LINE \      logger::utils::LoggerUtils::getFileAndLine(__FILE__, __LINE__) #define FUNC_NAME \      logger::utils::LoggerUtils::getFuncName(__FUNCTION__) #define FUNC_ENTRY_NAME \      logger::utils::LoggerUtils::getInFuncName(__FUNCTION__) #define FUNC_EXIT_NAME \      logger::utils::LoggerUtils::getOutFuncName(__FUNCTION__) #endif /* LOGGERMACROSES_HPP_ */ ```** 14. 创建一个名为**的头文件和源文件**。在 **StreamLogger.hpp** 中,添加所需的包含防护装置。包括 **LoggerMacroses.hpp** 和 **LoggerUtils.hpp** 头文件。然后,包括用于**标准::互斥流**支持的 **<流>头,用于**标准::线程**支持的**线程<头,以及用于**标准::互斥流**支持的 **<互斥流>头****** 15. 进入`名称空间`记录器。声明`StreamLogger`类,该类继承自`std::ostringstream`类。这种继承允许我们使用重载的左移位操作符< <进行记录。我们不设置输出设备,因此不会执行输出–只是存储在内部缓冲器中。在私有部分,声明一个名为`m_mux`的静态`std::mutex`变量。声明常量字符串,以便可以存储日志级别、文件和行以及函数名。在公共部分中,声明一个构造函数,该构造函数将日志级别、文件和行以及函数名作为参数。声明一个类析构函数。类声明应该如下所示: ```cpp namespace logger { class StreamLogger : public std::ostringstream { public:      StreamLogger(const std::string logLevel,                   const std::string fileLine,                   const std::string funcName);      ~StreamLogger(); private:      static std::mutex m_mux;      const std::string m_logLevel;      const std::string m_fileLine;      const std::string m_funcName; }; } // namespace logger ``` 16. 在`StreamLogger.cpp`中,包含`StreamLogger.hpp`和`< iostream >`头,用于`std::cout`支持。进入`记录器`命名空间。定义构造函数并初始化初始化列表中的所有成员。然后,定义析构函数并输入它的作用域。锁定`m_mux`互斥体。如果内部缓冲区为空,则只输出日期和时间、线程 ID、日志级别、文件和行以及函数名。因此,我们将得到如下格式的行:`【datetime】【threadId】【logLevel】【文件:line:】【name()-】`。如果内部缓冲区包含任何数据,输出与缓冲区结尾相同的字符串。因此,我们将得到如下格式的行:`【datetime】【threadId】【log level】【文件:行:】【name()-】|消息`。完整的源文件应该如下所示: ```cpp #include "StreamLogger.hpp" #include std::mutex logger::StreamLogger::m_mux; namespace logger { StreamLogger::StreamLogger(const std::string logLevel,                   const std::string fileLine,                   const std::string funcName)           : m_logLevel(logLevel)           , m_fileLine(fileLine)           , m_funcName(funcName) {} StreamLogger::~StreamLogger() {      std::lock_guard lock(m_mux);      if (this->str().empty())      {           std::cout << DATETIME << THREAD_ID << m_logLevel << m_fileLine << m_funcName << std::endl;      }      else      {           std::cout << DATETIME << THREAD_ID << m_logLevel << m_fileLine << m_funcName << " | " << this->str() << std::endl;      } } } ``` 17. 创建一个名为`Logger.hpp`的头文件,并添加所需的包含防护。包括`StreamLogger.hpp`和`LoggerMacroses.hpp`头。接下来,为不同的日志记录级别创建宏定义:`LOG_TRACE()`、`LOG_DEBUG()`、`LOG_WARN()`、`LOG_TRACE()`、`LOG_INFO()`、`LOG_ERROR()`、`LOG_TRACE_ENTRY()`、`LOG_TRACE_EXIT()`。完整的头文件应该如下所示: ```cpp #ifndef LOGGER_HPP_ #define LOGGER_HPP_ #include "StreamLogger.hpp" #include "LoggerMacroses.hpp" #define LOG_TRACE() logger::StreamLogger{LOG_LEVEL("Trace"), FILE_LINE, FUNC_NAME} #define LOG_DEBUG() logger::StreamLogger{LOG_LEVEL("Debug"), FILE_LINE, FUNC_NAME} #define LOG_WARN() logger::StreamLogger{LOG_LEVEL("Warning"), FILE_LINE, FUNC_NAME} #define LOG_TRACE() logger::StreamLogger{LOG_LEVEL("Trace"), FILE_LINE, FUNC_NAME} #define LOG_INFO() logger::StreamLogger{LOG_LEVEL("Info"), FILE_LINE, FUNC_NAME} #define LOG_ERROR() logger::StreamLogger{LOG_LEVEL("Error"), FILE_LINE, FUNC_NAME} #define LOG_TRACE_ENTRY() logger::StreamLogger{LOG_LEVEL("Error"), FILE_LINE, FUNC_ENTRY_NAME} #define LOG_TRACE_EXIT() logger::StreamLogger{LOG_LEVEL("Error"), FILE_LINE, FUNC_EXIT_NAME} #endif /* LOGGER_HPP_ */ ``` 18. 用适当的宏定义调用替换所有的`标准::cout`调用。在`Watchman.cpp`源文件中包含`logger/Logger.hpp`头文件。在`runAdd()`功能中,用不同日志级别的宏定义替换`std::cout`的所有实例。`运行添加()`功能应该如下所示: ```cpp void Watchman::runAdd() {      while (true)      {           std::unique_lock locker(m_AddMux);           while(!m_AddNotified)           {                LOG_DEBUG() << "Spurious awakening";                m_CondVarAddPerson.wait(locker);           }           LOG_INFO() << "New person came";           m_AddNotified = false;           while (m_CreatedPeople.size() > 0)           {                try                {                     auto person = m_CreatedPeople.get();                     if (m_PeopleInside.size() < CountPeopleInside)                     {                          LOG_INFO() << "Welcome in the our Art Gallery";                          m_PeopleInside.add(std::move(person));                     }                     else                     {                          LOG_INFO() << "Sorry, we are full. Please wait";                          m_PeopleInQueue.add(std::move(person));                     }                }                catch(const std::string& e)                {                     LOG_ERROR() << e;                }           }           LOG_TRACE() << "Check people in queue";           if (m_PeopleInQueue.size() > 0)           {                while (m_PeopleInside.size() < CountPeopleInside)                {                     try                     {                          auto person = m_PeopleInQueue.get();                          LOG_INFO() << "Welcome in the our Art Gallery";                          m_PeopleInside.add(std::move(person));                     }                     catch(const std::string& e)                     {                          LOG_ERROR() << e;                     }                }           }      } } ``` 19. 注意我们如何使用新的记录器。我们用括号调用宏定义,并使用左移位运算符: ```cpp LOG_ERROR() << e; Or LOG_INFO() << "Welcome in the our Art Gallery"; ``` 20. 对其余代码进行同样的替换。 21. 构建并运行应用。在终端中,您将看到日志消息以不同的日志级别和有用的信息从不同的线程中出现。过了一段时间后,您将获得类似以下内容的输出: ![Figure 6.26: The execution result of the activity project ](img/C14583_06_26.jpg) ###### 图 6.26:活动项目的执行结果 如您所见,阅读和理解日志非常容易。如果您的需求不同,您可以很容易地更改`StreamLogger`类,将日志写入文件系统上的文件。您可以添加使用日志调试应用所需的任何其他信息,例如输出函数参数。您还可以为自定义类型重写左移位运算符,以便轻松输出调试信息。 在这个项目中,我们采用了许多我们在本章中学到的东西。我们为线程安全的输出创建了一个额外的流,我们将输出格式化为所需的表示,我们使用`std::stringstream`来执行格式化数据,我们使用宏定义来方便记录器的使用。因此,这个项目展示了我们处理并发输入/输出的技能 ## 第 7 章-每个人都会跌倒,这是你爬起来的方式-测试和调试 ### 活动 1:使用测试用例检查功能的准确性并理解测试驱动开发(TDD) 对于本活动,我们将开发解析 **RecordFile.txt** 和 **CurrencyConversion.txt** 文件的函数,并编写测试用例来检查函数的准确性。按照以下步骤实施本活动: 1. 创建一个名为 **parse.conf** 的配置文件并编写配置。 2. 注意这里只关注两个变量,即`current file`和`recordFile`。其余的用于其他环境变量: ```cpp CONFIGURATION_FILE currencyFile = ./CurrencyConversion.txt recordFile = ./RecordFile.txt DatabaseServer = 192.123.41.112 UserId = sqluser Password = sqluser RestApiServer = 101.21.231.11 LogFilePath = /var/project/logs ``` 3. 创建一个名为`CommonHeader.h`的头文件,并声明所有的实用函数,即`isAllNumbers()`、`isDigit()`、`parceline()`、`checkFile()`、`parseConfig()`、`parsecurrency parameters()`、`fillCurrencyMap()`、`recordparsefile()`、 ```cpp #ifndef __COMMON_HEADER__H #define __COMMON_HEADER__H #include #include #include #include #include #include #include #include #include #include using namespace std; // Forward declaration of global variables. extern string configFile; extern string recordFile; extern string currencyFile; extern map currencyMap; struct record; extern vector      vecRecord; //Structure to hold Record Data . struct record{     int     customerId;     string  firstName;     string  lastName;     int     orderId;     int     productId;     int     quantity;     float   totalPriceRegional;     string  currency;     float   totalPriceUsd;     record(vector & in){         customerId      = atoi(in[0].c_str());         firstName       = in[1];         lastName        = in[2];         orderId         = atoi(in[3].c_str());         productId       = atoi(in[4].c_str());         quantity        = atoi(in[5].c_str());         totalPriceRegional = static_cast(atof(in[6].c_str()));         currency        = in[7];         totalPriceUsd   = static_cast(atof(in[8].c_str()));     } }; // Declaration of Utility Functions.. string trim (string &); bool isAllNumbers(const string &); bool isDigit(const string &); void parseLine(ifstream &, vector &, char); bool checkFile(ifstream &, string &, string, char, string &); bool parseConfig(); bool parseCurrencyParameters( vector &); bool fillCurrencyMap(); bool parseRecordFile(); bool checkRecord(vector &); void displayCurrencyMap(); ostream& operator<<(ostream &, const record &); void displayRecords(); #endif ``` 4. 创建一个名为 **Util.cpp** 的文件,定义所有的实用函数。编写以下代码定义`修剪()`功能: ```cpp #include // Utility function to remove spaces and tabs from start of string and end of string.. string trim (string &str) { // remove space and tab from string.     string res("");     if ((str.find(' ') != string::npos) || (str.find(' ') != string::npos)){ // if space or tab found..         size_t begin, end;         if ((begin = str.find_first_not_of(" \t")) != string::npos){ // if string is not empty..             end = str.find_last_not_of(" \t");             if ( end >= begin )                 res = str.substr(begin, end - begin + 1);         }     }else{         res = str; // No space or tab found..     }     str = res;     return res; } ``` 5. 编写以下代码来定义`isAllNumbers()`、`isDigit()`和`parceline()`函数: ```cpp // Utility function to check if string contains only digits ( 0-9) and only single '.' // eg . 1121.23 , .113, 121\. are valid, but 231.14.143 is not valid. bool isAllNumbers(const string &str){ // make sure, it only contains digit and only single '.' if any     return ( all_of(str.begin(), str.end(), [](char c) { return ( isdigit(c) || (c == '.')); })              && (count(str.begin(), str.end(), '.') <= 1) ); } //Utility function to check if string contains only digits (0-9).. bool isDigit(const string &str){     return ( all_of(str.begin(), str.end(), [](char c) { return isdigit(c); })); } // Utility function, where single line of file is parsed using delimiter. // And store the tokens in vector of string. void parseLine(ifstream &infile, vector & vec, char delimiter){     string line, token;     getline(infile, line);     istringstream ss(line);     vec.clear();     while(getline(ss, token, delimiter)) // break line using delimiter         vec.push_back(token);  // store tokens in vector of string } ``` 6. 编写以下代码来定义`解析当前参数()`和`检查记录()`函数: ```cpp // Utility function to check if vector string of 2 strings contain correct // currency and conversion ratio. currency should be 3 characters, conversion ratio // should be in decimal number format. bool parseCurrencyParameters( vector & vec){     trim(vec[0]);  trim(vec[1]);     return ( (!vec[0].empty()) && (vec[0].size() == 3) && (!vec[1].empty()) && (isAllNumbers(vec[1])) ); } // Utility function, to check if vector of string has correct format for records parsed from Record File. // CustomerId, OrderId, ProductId, Quantity should be in integer format // TotalPrice Regional and USD should be in decimal number format // Currecny should be present in map. bool checkRecord(vector &split){     // Trim all string in vector     for (auto &s : split)         trim(s);     if ( !(isDigit(split[0]) && isDigit(split[3]) && isDigit(split[4]) && isDigit(split[5])) ){         cerr << "ERROR: Record with customer id:" << split[0] << " doesnt have right DIGIT parameter" << endl;         return false;     }     if ( !(isAllNumbers(split[6]) && isAllNumbers(split[8])) ){         cerr << "ERROR: Record with customer id:" << split[0] << " doesnt have right NUMBER parameter" << endl;         return false;     }     if ( currencyMap.find(split[7]) == currencyMap.end() ){         cerr << "ERROR: Record with customer id :" << split[0] << " has currency :" << split[7] << " not present in map" << endl;         return false;     }     return true; } ``` 7. 编写以下代码定义`检查文件()`功能: ```cpp // Function to test initial conditions of file.. // Check if file is present and has correct header information. bool checkFile(ifstream &inFile, string &fileName, string parameter, char delimiter, string &error){     bool flag = true;     inFile.open(fileName);     if ( inFile.fail() ){         error = "Failed opening " + fileName + " file, with error: " + strerror(errno);         flag = false;     }     if (flag){         vector split;         // Parse first line as header and make sure it contains parameter as first token.         parseLine(inFile, split, delimiter);         if (split.empty()){             error = fileName + " is empty";             flag = false;         } else if ( split[0].find(parameter) == string::npos ){             error = "In " + fileName + " file, first line doesnt contain header ";             flag = false;         }     }     return flag; } ``` 8. 编写以下代码定义`parseConfig()`函数: ```cpp // Function to parse Config file. Each line will have ' = format // Store CurrencyConversion file and Record File parameters correctly. bool parseConfig() {     ifstream coffle;     string error;     if (!checkFile(confFile, configFile, "CONFIGURATION_FILE", '=', error)){         cerr << "ERROR: " << error << endl;         return false;     }     bool flag = true;     vector split;     while (confFile.good()){         parseLine(confFile, split, '=');         if ( split.size() == 2 ){             string name = trim(split[0]);             string value = trim(split[1]);             if ( name == "currencyFile" )                 currencyFile = value;             else if ( name == "recordFile")                 recordFile = value;         }     }     if ( currencyFile.empty() || recordFile.empty() ){         cerr << "ERROR : currencyfile or recordfile not set correctly." << endl;         flag = false;     }     return flag; } ``` 9. 编写以下代码来定义`filllcurrency map()`函数: ```cpp // Function to parse CurrencyConversion file and store values in Map. bool fillCurrencyMap() {     ifstream currFile;     string error;     if (!checkFile(currFile, currencyFile, "Currency", '|', error)){         cerr << "ERROR: " << error << endl;         return false;     }     bool flag = true;     vector split;     while (currFile.good()){         parseLine(currFile, split, '|');         if (split.size() == 2){             if (parseCurrencyParameters(split)){                 currencyMap[split[0]] = static_cast(atof(split[1].c_str())); // make sure currency is valid.             } else {                 cerr << "ERROR: Processing Currency Conversion file for Currency: "<< split[0] << endl;                 flag = false;                 break;             }         } else if (!split.empty()){             cerr << "ERROR: Processing Currency Conversion , got incorrect parameters for Currency: " << split[0] << endl;             flag = false;             break;         }     }     return flag; } ``` 10. 编写以下代码来定义`parseRecordFile()`函数: ```cpp // Function to parse Record File .. bool parseRecordFile(){     ifstream recFile;     string error;     if (!checkFile(recFile, recordFile, "Customer Id", '|', error)){         cerr << "ERROR: " << error << endl;         return false;     }     bool flag = true;     vector split;     while(recFile.good()){         parseLine(recFile, split, '|');         if (split.size() == 9){             if (checkRecord(split)){                 vecRecord.push_back(split); //Construct struct record and save it in vector...             }else{                 cerr << "ERROR : Parsing Record, for Customer Id: " << split[0] << endl;                 flag = false;                 break;             }         } else if (!split.empty()){             cerr << "ERROR: Processing Record, for Customer Id: " << split[0] << endl;             flag = false;             break;         }     }     return flag; } ``` 11. 编写以下代码定义`displayCurrencyMap()`函数: ```cpp void displayCurrencyMap(){     cout << "Currency MAP :" << endl;     for (auto p : currencyMap)         cout << p.first <<"  :  " << p.second << endl;     cout << endl; } ostream& operator<<(ostream& os, const record &rec){     os << rec.customerId <<"|" << rec.firstName << "|" << rec.lastName << "|"        << rec.orderId << "|" << rec.productId << "|" << rec.quantity << "|"        << fixed << setprecision(2) << rec.totalPriceRegional << "|" << rec.currency << "|"        << fixed << setprecision(2) << rec.totalPriceUsd << endl;     return os; } ``` 12. 编写以下代码定义`显示记录()`功能: ```cpp void displayRecords(){     cout << " Displaying records with '|' delimiter" << endl;     for (auto rec : vecRecord){         cout << rec;     }     cout << endl; } ``` 13. 创建一个名为 **ParseFiles.cpp** 的文件,并调用`parseConfig()`、`filllcurrency map()`和`parseRecordFile()`函数: ```cpp #include // Global variables ... string configFile = "./parse.conf"; string recordFile; string currencyFile; map  currencyMap; vector      vecRecord; int main(){     // Read Config file to set global configuration variables.     if (!parseConfig()){         cerr << "Error parsing Config File " << endl;         return false;     }     // Read Currency file and fill map     if (!fillCurrencyMap()){         cerr << "Error setting CurrencyConversion Map " << endl;         return false;     }     if (!parseRecordFile()){         cerr << "Error parsing Records File " << endl;         return false;     }         displayCurrencyMap();     displayRecords();     return 0; } ``` 14. Open the compiler. Compile and execute the **Util.cpp** and **ParseFiles.cpp** files by writing the following command: ```cpp g++ -c -g -I. -Wall Util.cpp g++ -g -I. -Wall Util.o ParseFiles.cpp -o ParseFiles ``` 将生成两者的二进制文件。 在下面的截图中,您将看到这两个命令都存储在 build.sh 脚本中并被执行。运行此脚本后,您将看到最新的`Util.o`和`ParseFiles`文件已经生成: ![Figure 7.25: New files generated ](img/C14583_07_25.jpg) ###### 图 7.25:生成的新文件 15. After running the `ParseFiles` executable, we'll receive the following output: ![Figure 7.26: New files generated ](img/C14583_07_26.jpg) ###### 图 7.26:生成的新文件 16. 创建一个名为**的文件,并为实用函数编写测试用例。为**修剪**功能编写以下测试用例: ```cpp #include #include"../CommonHeader.h" using namespace std; // Global variables ... string configFile = "./parse.conf"; string recordFile; string currencyFile; map  currencyMap; vector      vecRecord; void setDefault(){     configFile = "./parse.conf";     recordFile.clear();     currencyFile.clear();     currencyMap.clear();     vecRecord.clear(); } // Test Cases for trim function ... TEST(trim, empty){     string str="    ";     EXPECT_EQ(trim(str), string()); } TEST(trim, start_space){     string str = "   adas";     EXPECT_EQ(trim(str), string("adas")); } TEST(trim, end_space){     string str = "trip      ";     EXPECT_EQ(trim(str), string("trip")); } TEST(trim, string_middle){     string str = "  hdgf   ";     EXPECT_EQ(trim(str), string("hdgf")); } TEST(trim, single_char_start){     string str = "c  ";     EXPECT_EQ(trim(str), string("c")); } TEST(trim, single_char_end){     string str = "   c";     EXPECT_EQ(trim(str), string("c")); } TEST(trim, single_char_middle){     string str = "      c  ";     EXPECT_EQ(trim(str), string("c")); } ```** 17. 为`isAllNumbers`函数编写以下测试用例: ```cpp // Test Cases for isAllNumbers function.. TEST(isNumber, alphabets_present){     string str = "11.qwe13";     ASSERT_FALSE(isAllNumbers(str)); } TEST(isNumber, special_character_present){     string str = "34.^%3";     ASSERT_FALSE(isAllNumbers(str)); } TEST(isNumber, correct_number){     string str = "54.765";     ASSERT_TRUE(isAllNumbers(str)); } TEST(isNumber, decimal_begin){     string str = ".624";     ASSERT_TRUE(isAllNumbers(str)); } TEST(isNumber, decimal_end){     string str = "53.";     ASSERT_TRUE(isAllNumbers(str)); } ``` 18. 为`isDigit`函数编写以下测试用例: ```cpp // Test Cases for isDigit funtion... TEST(isDigit, alphabet_present){     string str = "527A";     ASSERT_FALSE(isDigit(str)); } TEST(isDigit, decimal_present){     string str = "21.55";     ASSERT_FALSE(isDigit(str)); } TEST(isDigit, correct_digit){     string str = "9769";     ASSERT_TRUE(isDigit(str)); } ``` 19. 为`解析当前参数`函数编写以下测试用例: ```cpp // Test Cases for parseCurrencyParameters function TEST(CurrencyParameters, extra_currency_chararcters){     vector vec {"ASAA","34.22"};     ASSERT_FALSE(parseCurrencyParameters(vec)); } TEST(CurrencyParameters, correct_parameters){     vector vec {"INR","1.44"};     ASSERT_TRUE(parseCurrencyParameters(vec)); } ``` 20. Write the following test cases for the `checkFile` function: ```cpp //Test Cases for checkFile function... TEST(checkFile, no_file_present){     string fileName = "./NoFile";     ifstream infile;     string parameter("nothing");     char delimit =';';     string err;     ASSERT_FALSE(checkFile(infile, fileName, parameter, delimit, err)); } TEST(checkFile, empty_file){     string fileName = "./emptyFile";     ifstream infile;     string parameter("nothing");     char delimit =';';     string err;     ASSERT_FALSE(checkFile(infile, fileName, parameter, delimit, err)); } TEST(checkFile, no_header){     string fileName = "./noHeaderFile";     ifstream infile;     string parameter("header");     char delimit ='|';     string err;     ASSERT_FALSE(checkFile(infile, fileName, parameter, delimit, err)); } TEST(checkFile, incorrect_header){     string fileName = "./correctHeaderFile";     ifstream infile;     string parameter("header");     char delimit ='|';     string err;     ASSERT_FALSE(checkFile(infile, fileName, parameter, delimit, err)); } TEST(checkFile, correct_file){     string fileName = "./correctHeaderFile";     ifstream infile;     string parameter("Currency");     char delimit ='|';     string err;     ASSERT_TRUE(checkFile(infile, fileName, parameter, delimit, err)); } ``` #### 注意 在前面的函数中用作输入参数的 **NoFile** 、 **emptyFile** 、 **noHeaderFile** 、**correcheader file**文件可以在这里找到:[https://github . com/trainingypbackt/Advanced-CPlusPlus/tree/master/lesson 7/activity 01](https://github.com/TrainingByPackt/Advanced-CPlusPlus/tree/master/Lesson7/Activity01)。 21. Write the following test cases for the `parseConfig` function: ```cpp //Test Cases for parseConfig function... TEST(parseConfig, missing_currency_file){     setDefault();     configFile = "./parseMissingCurrency.conf";     ASSERT_FALSE(parseConfig()); } TEST(parseConfig, missing_record_file){     setDefault();     configFile = "./parseMissingRecord.conf";     ASSERT_FALSE(parseConfig()); } TEST(parseConfig, correct_config_file){     setDefault();     configFile = "./parse.conf";     ASSERT_TRUE(parseConfig()); } ``` #### 注意 在前面的函数中用作输入参数的**parsemissingcurrency . conf**、 **parseMissingRecord.conf** 和 **parse.conf** 文件可以在这里找到:[https://github . com/trainingypbackt/Advanced-CPlusPlus/tree/master/lesson 7/activity 01](https://github.com/TrainingByPackt/Advanced-CPlusPlus/tree/master/Lesson7/Activity01)。 22. Write the following test cases for the `fillCurrencyMap` function: ```cpp //Test Cases for fillCurrencyMap function... TEST(fillCurrencyMap, wrong_delimiter){     currencyFile = "./CurrencyWrongDelimiter.txt";     ASSERT_FALSE(fillCurrencyMap()); } TEST(fillCurrencyMap, extra_column){     currencyFile = "./CurrencyExtraColumn.txt";     ASSERT_FALSE(fillCurrencyMap()); } TEST(fillCurrencyMap, correct_file){     currencyFile = "./CurrencyConversion.txt";     ASSERT_TRUE(fillCurrencyMap()); } ``` #### 注意 在前面的函数中用作输入参数的**currency erriddelimiter . txt**、 **CurrencyExtraColumn.txt** 、 **CurrencyConversion.txt** 文件可以在这里找到:[https://github . com/trainingypbackt/Advanced-CPlusPlus/tree/master/lesson 7/activity 01](https://github.com/TrainingByPackt/Advanced-CPlusPlus/tree/master/Lesson7/Activity01)。 23. Write the following test cases for the parseRecordFile function: ```cpp //Test Cases for parseRecordFile function... TEST(parseRecordFile, wrong_delimiter){     recordFile = "./RecordWrongDelimiter.txt";     ASSERT_FALSE(parseRecordFile()); } TEST(parseRecordFile, extra_column){     recordFile = "./RecordExtraColumn.txt";     ASSERT_FALSE(parseRecordFile()); } TEST(parseRecordFile, correct_file){     recordFile = "./RecordFile.txt";     ASSERT_TRUE(parseRecordFile()); } ``` 在前面的函数中用作输入参数的**recorderry delimiter . txt**、**recordextrectory column . txt**、 **RecordFile.txt** 文件可以在这里找到:[https://github . com/trainingypbackt/Advanced-CPlusPlus/tree/master/lesson 7/activity 01](https://github.com/TrainingByPackt/Advanced-CPlusPlus/tree/master/Lesson7/Activity01)。 24. Open the compiler. Compile and execute the `Util.cpp` and `ParseFileTestCases.cpp` files by writing the following commands: ```cpp g++ -c -g -Wall ../Util.cpp -I../ g++ -c -g -Wall ParseFileTestCases.cpp g++ -g -Wall Util.o ParseFileTestCases.o -lgtest -lgtest_main -pthread -o ParseFileTestCases ``` 下面是这个的截图。您将看到所有存储在`Test.make`脚本文件中的命令。一旦执行,它将创建用于单元测试的二进制程序,称为`解析文件测试用例`。您还会注意到,在项目中创建了一个名为`单元测试`的目录。在这个目录中,所有与单元测试相关的代码都被写入,并且一个二进制文件被创建。此外,项目的依赖库`Util.o`也是通过在`Util.cpp`文件中编译项目而创建的: ![](img/C14583_07_27.jpg) ###### 图 7.27:执行脚本文件中的所有命令 25. Type the following command to run all the test cases: ```cpp ./ParseFileTestCases ``` 屏幕上的输出将显示运行的全部测试,即 8 个测试套件中的 31 个。它还将显示单个测试套件的统计数据以及通过/失败结果: ![Figure 7.28: All tests running properly ](img/C14583_07_28.jpg) ###### 图 7.28:所有测试运行正常 以下是接下来测试的截图: ![Figure 7.29: All tests running properly ](img/C14583_07_29.jpg) ###### 图 7.29:所有测试运行正常 最后,我们通过在测试用例的帮助下解析两个文件来检查我们开发的函数的准确性。这将确保我们的项目在与具有测试用例的不同功能/模块集成时运行良好。 ## 第 8 章-速度需求-性能和优化 ### 活动 1:优化拼写检查算法 在本活动中,我们将开发一个简单的拼写检查演示,并尝试让它变得更快。可以用骨架文件 **Speller.cpp** 作为起点。执行以下步骤来实施本活动: 1. 对于拼写检查的第一个实现(完整代码可以在 **Speller1.cpp** 中找到)–在`get 拼错()`函数中创建字典集: ```cpp set setDict(vecDict.begin(), vecDict.end()); ``` 2. 循环检查文本单词,并使用`set::count()`方法检查字典中没有的单词。将拼错的单词添加到结果向量中: ```cpp vector ret; for(int i = 0; i < vecText.size(); ++ i) {   const string &s = vecText[i];   if(!setDict.count(s))   {     ret.push_back(i);   } }; ``` 3. Open the terminal. Compile the program and run it as follows: ```cpp $ g++ -O3 Speller1.cpp Timer.cpp $ ./a.out ``` 将生成以下输出: ![Figure 8.60: Example output of the solution for Step 1 ](img/C14583_08_60.jpg) ###### 图 8.60:步骤 1 解决方案的示例输出 4. 打开 **Speller2.cpp** 文件,将`无序 _set`头文件添加到程序中: ```cpp #include ``` 5. 接下来,将字典使用的集合类型更改为`无序 _ 集合` : ```cpp unordered_set setDict(vecDict.begin(), vecDict.end()); ``` 6. Open the Terminal. Compile the program and run it as follows: ```cpp $ g++ -O3 Speller2.cpp Timer.cpp $ ./a.out ``` 将生成以下输出: ![Figure 8.61: Example output of the solution for Step 2 ](img/C14583_08_61.jpg) ###### 图 8.61:步骤 2 解决方案的示例输出 7. For the third and final version, that is, **Speller3.cpp**, we will use a bloom filter. Start by defining a hash function based on the `BKDR` function. Add the following code to implement this: ```cpp const size_t SIZE = 16777215; template size_t hasher(const string &s) {   size_t h = 0;   size_t len = s.size();   for(size_t i = 0; i < len; i++)   {     h = h * SEED + s[i];   }   return h & SIZE; } ``` 这里,我们使用了一个整数模板参数,这样我们就可以用相同的代码创建任意数量的不同散列函数。注意`16777215`常量的使用,它等于`2^24–1`。这让我们可以使用快速按位“与”运算符而不是“模”运算符来保持散列整数小于`大小`。如果你想改变大小,保持它小于二的 1 次方。 8. 接下来,让我们为**中的布隆过滤器声明一个向量,并用字典中的单词填充它。使用三个散列函数。BKDR 散列可以被植入诸如 **131** 、 **3131** 、 **31313** 等值。添加以下代码来实现: ```cpp vector m_Bloom; m_Bloom.resize(SIZE); for(auto i = vecDict.begin(); i != vecDict.end(); ++ i) {   m_Bloom[hasher<131>(*i)] = true;   m_Bloom[hasher<3131>(*i)] = true;   m_Bloom[hasher<31313>(*i)] = true; } ```** 9. Write the following code to create a loop that checks the words: ```cpp for(int i = 0; i < vecText.size(); ++ i) {   const string &s = vecText[i];   bool hasNoBloom =           !m_Bloom[hasher<131>(s)]       &&  !m_Bloom[hasher<3131>(s)]       &&  !m_Bloom[hasher<31313>(s)];   if(hasNoBloom)   {     ret.push_back(i);   }   else if(!setDict.count(s))   {     ret.push_back(i);   } } ``` 首先检查布隆过滤器,如果它在字典中找到这个词,我们必须验证它,就像我们以前做的那样。 10. Open the terminal. Compile the program and run it as follows: ```cpp $ g++ -O3 Speller3.cpp Timer.cpp $ ./a.out ``` 将生成以下输出: ![Figure 8.62: Example output of the solution for Step 3 ](img/C14583_08_62.jpg) ###### 图 8.62:步骤 3 解决方案的示例输出 在前面的活动中,我们试图解决一个现实世界的问题,并使其更加有效。让我们考虑三个步骤中每个实现的一些要点,如下所示: * 对于第一个版本,使用了带有`std::set`的最明显的解决方案–然而,性能可能较低,因为 set 数据结构基于二叉树,该二叉树具有`O(log N)`的复杂性来寻找元素。 * 对于第二个版本,我们只需切换到`std::unordered_set`,就可以获得很大的性能提升,它使用哈希表作为底层数据结构。如果哈希函数好,性能会接近`O(1)`。 * 第三个版本基于**布隆过滤器**数据结构,需要一些考虑。bloom filter 的主要性能优势是因为它是一个紧凑的数据结构,实际上并没有在其中存储实际的元素,因此提供了非常好的缓存性能。 从实现的角度来看,以下准则适用: * `向量<布尔>`可以用作后备存储,因为它是存储和检索位的有效方式。 * 布隆过滤器的假阳性百分比应该是最小的——任何超过 5%的都是无效的。 * 有许多字符串哈希算法–引用实现中使用了 **BKDR** 哈希算法。一个全面的字符串哈希算法及其实现可以在这里找到:[http://www.partow.net/programming/hashfunctions/index.html](http://www.partow.net/programming/hashfunctions/index.html)。 * 哈希函数的数量和使用的 bloom 过滤器的大小对于获得性能优势非常关键。 * 在决定布隆过滤器应该使用什么参数时,应该考虑数据集的性质——考虑到在这个例子中,拼错的单词很少,而且大部分都在字典中。 鉴于我们收到的结果,有一些问题值得探讨: * 为什么布隆过滤器的性能提升如此微弱? * 使用更大或更小容量的 Bloom 过滤器会有什么影响? * 当使用更少或更多的散列函数时会发生什么? * 在什么条件下这个版本会比 **Speller2.cpp** 中的版本快很多? 以下是这些问题的答案: * Why is the improvement in performance so meager with the Bloom Filter? `std::unordered_set`在达到存储的值之前,执行一次哈希操作,可能还会执行几次内存访问。我们使用的布隆过滤器执行三次哈希操作和三次内存访问。因此,本质上,布隆过滤器所做的工作不仅仅是哈希表。由于我们的字典中只有 31,870 个单词,因此布隆过滤器的缓存优势就丧失了。这是另一种情况,在这种情况下,由于缓存,传统的数据结构分析与实际结果不一致。 * What is the effect of using a larger or smaller capacity Bloom filter? 当使用更大的容量时,哈希冲突的数量会减少,误报也会减少,但缓存行为会恶化。相反,当使用较小的容量时,哈希冲突和误报会增加,但缓存行为会改善。 * What happens when fewer or more hash functions are used? 使用的散列函数越多,误报就越少,反之亦然。 * Under what conditions would this version be much faster than the one in Speller2.cpp? 当测试几个位的成本小于访问哈希表中的值的成本时,Bloom 过滤器工作得最好。只有当布隆过滤器位完全适合缓存,而字典不适合时,这种情况才会出现。 ================================================ FILE: docs/adv-cpp/README.md ================================================ # C++ 高级编程 > 原书:[Advanced C++](https://libgen.rs/book/index.php?md5=24E080E694C59B3F8E0220D0902724B0) > > 协议:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) > > 阶段:机翻(1) > > 自豪地采用[谷歌翻译](https://translate.google.cn/) > > 让开源界提前感受入关。 * [在线阅读](https://ccpp.apachecn.org) * [在线阅读(Gitee)](https://apachecn.gitee.io/apachecn-c-cpp-zh/) * [ApacheCN 学习资源](http://docs.apachecn.org/) ## 贡献指南 本项目需要校对,欢迎大家提交 Pull Request。 > 请您勇敢地去翻译和改进翻译。虽然我们追求卓越,但我们并不要求您做到十全十美,因此请不要担心因为翻译上犯错——在大部分情况下,我们的服务器已经记录所有的翻译,因此您不必担心会因为您的失误遭到无法挽回的破坏。(改编自维基百科) ## 联系方式 ### 负责人 * [飞龙](https://github.com/wizardforcel): 562826179 ### 其他 * 在我们的 [apachecn/apachecn-c-cpp-zh](https://github.com/apachecn/apachecn-c-cpp-zh) github 上提 issue. * 发邮件到 Email: `apachecn@163.com`. * 在我们的 [组织学习交流群](http://www.apachecn.org/organization/348.html) 中联系群主/管理员即可. ## 赞助我们 ![](http://data.apachecn.org/img/about/donate.jpg) ================================================ FILE: docs/adv-cpp/SUMMARY.md ================================================ + [C++ 高级编程](README.md) + [零、前言](00.md) + [一、可移植的 C++ 软件剖析](01.md) + [二、不允许鸭子——类型和推导(一)](02.md) + [三、不允许鸭子——模板和推导(二)](03.md) + [四、不允许泄漏——异常和资源](04.md) + [五、关注点分离——软件架构、函数和可变模板](05.md) + [六、哲学家的晚餐——线程和并发](06.md) + [七、流和输入/输出](07.md) + [八、每个人都会跌倒,这是你爬起来的方式——测试和调试](08.md) + [九、对速度的需求——性能和优化](09.md) + [十、附录](10.md) ================================================ FILE: docs/adv-cpp-prog-cb/00.md ================================================ # 零、前言 在这本书里,你将学习高级的 C++ 技术,你可以在你自己的 C++ 项目中使用。这本书教 C++ 使用的是一种食谱风格的方法,每种食谱都有例子和截图,你可以从 GitHub 下载并自己动手。这本书使用 C++ 17 规范来教 C++,并在最后偷偷看了一下 C++ 20 中增加的新特性。在一些食谱中,我们甚至会使用反汇编器来更好地理解 C++ 是如何编译的,以及某些决定对您的应用的影响。到本书结束时,您将掌握 C++ 的高级概念,并能够解决日常问题,这将使您的 C++ 编程更上一层楼。 # 这本书是给谁的 本书面向熟悉 C++ 并希望获得专家技能,成为精通 C++ 开发人员的中级 C++ 开发人员。假设对语言有很好的理解,包括对汇编的基本理解。 # 这本书涵盖了什么 [第 1 章](01.html)*入门库开发*,教你如何开发自己的库,包括一个最少惊喜原则的解释,如何命名一切,如何编写只有头文件的库,以及如何保证别人会继续使用你的库。 [第 2 章](02.html)、*使用异常进行错误处理*,涵盖了 C++ 异常和错误处理的更高级的主题,包括对`noexcept`说明符和运算符的详细解释,RAII 如何在出现异常时支持资源管理,为什么应该避免从析构函数抛出,以及如何编写自己的异常。 [第三章](03.html)、*实现移动语义*,提供了 C++ 移动语义的详细解释,包括对*大五*的解释,如何让你的类可移动,如何编写只移动(和不移动)不复制样式的类,如何正确实现一个移动构造函数,为什么`const &&`没有意义,如何使用引用限定。 [第 4 章](04.html),*使用模板进行泛型编程*,像专家一样教你如何编写模板函数,包括如何实现自己的 SFINAE,如何执行完美转发,如何使用`constexpr-if`语句,如何利用带有参数包的元组,如何在编译时循环使用参数包,如何使用类型特征实现同一个函数的不同版本,如何使用`template`,以及如何在自己的应用中利用显式类型声明。 [第五章](05.html)、*并发和同步*,教你如何使用`std::mutex`(和朋友们),什么时候使用原子类型,如何使用`mutable`关键字处理`const`类的线程安全,如何编写线程安全类,如何编写线程安全包装器,以及如何编写包含承诺和未来的异步 C++ 语言。 [第 6 章](06.html)、*优化您的代码以获得性能*,涵盖了如何对您的 C++ 进行概要分析和基准测试,如何反汇编您的 C++ 以更好地理解如何优化您的代码,如何定位和删除不需要的内存分配,以及为什么`noexcept`有助于优化。 [第 7 章](07.html)、*调试和测试*,带您了解如何使用`Catch2`对 C++ 进行单元测试,如何使用谷歌的 ASAN 和 UBSAN 杀毒软件动态分析您的代码是否存在内存损坏和未定义的行为,以及如何使用 NDEBUG。 [第 8 章](08.html)、*创建和实现自己的容器*,通过创建一个始终排序的`std::vector`,教你如何编写自己的容器包装器。 [第 9 章](09.html),*探索类型擦除*,教你关于类型擦除需要知道的一切,包括如何通过继承和使用模板擦除类型,如何实现类型擦除模式,如何实现委托模式。 [第 10 章](10.html)、*深入了解动态分配*,教你动态内存分配方面的进阶话题,包括如何正确使用`std::unique_ptr`和`std::shared_ptr`、如何处理循环引用、如何键入强制转换智能指针,以及堆如何在幕后工作,为你的应用提供动态内存。 [第 11 章](11.html)、*C++ 中的常见模式*解释了计算机科学中不同的模式是如何在 c++ 中实现的,包括工厂模式、单例模式、装饰器模式和观察者模式,以及如何实现静态多态来编写自己的静态接口,而不需要虚拟继承。 [第 12 章](12.html)、*仔细看看类型推导*,深入探究了在 C++ 17 中如何进行类型推导,包括`auto`、`decltype`和`template`如何自动推导它们的类型。本章以如何编写自己的 C++ 17 用户定义推导指南的例子结束。 [第 13 章](13.html)*奖励:使用 C++ 20 特性*,提供了 C++ 20 新特性的预览,包括概念、模块、范围和协同程序。 # 充分利用这本书 我们假设您以前写过 C++ 并且已经熟悉了一些现代 C++ 特性。 这本书使用 Ubuntu 来提供例子,你可以在阅读这本书的时候自己编译和运行。我们假设您对 Ubuntu、如何安装它以及如何使用 Linux 终端有一些基本的了解。 我们在一些食谱中使用反汇编器来更好地理解编译器在幕后做什么。虽然您不需要知道如何阅读程序集来理解正在教授的内容,但是对 x86_64 程序集的基本理解将会有所帮助。 # 下载示例代码文件 你可以从你在[www.packt.com](http://www.packt.com)的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问[www.packtpub.com/support](https://www.packtpub.com/support)并注册将文件直接通过电子邮件发送给您。 您可以按照以下步骤下载代码文件: 1. 登录或注册[www.packt.com](http://www.packt.com)。 2. 选择“支持”选项卡。 3. 点击代码下载。 4. 在搜索框中输入图书的名称,并按照屏幕指示进行操作。 下载文件后,请确保使用最新版本的解压缩文件夹: * 视窗系统的 WinRAR/7-Zip * zipeg/izp/un ARX for MAC * 适用于 Linux 的 7-Zip/PeaZip 这本书的代码包也托管在 GitHub 上的[https://GitHub . com/packt publishing/Advanced-CPP-Programming-cook book](https://github.com/PacktPublishing/Advanced-CPP-Programming-CookBook)。如果代码有更新,它将在现有的 GitHub 存储库中更新。 我们还有来自丰富的图书和视频目录的其他代码包,可在**[【https://github.com/PacktPublishing/】](https://github.com/PacktPublishing/)**获得。看看他们! # 行动中的代码 访问以下链接查看正在运行的代码的视频:[https://bit.ly/2tQoZyW](https://bit.ly/2tQoZyW) # 使用的约定 本书通篇使用了许多文本约定。 `constexpr`:用文本、数字、文件夹名、文件名、文件扩展名、路径名、虚拟网址和用户输入表示码字。这里有一个例子:“使用`noexcept`说明符来告诉编译器一个函数是否可以抛出 C++ 异常。” 代码块设置如下: ```cpp int main(void) { the_answer is; return 0; } ``` 当我们希望将您的注意力吸引到代码块的特定部分时,相关的行或项目以粗体显示: ```cpp int main(void) { auto execute_on_exit = finally{[]{ std::cout << "The answer is: 42\n"; }}; } ``` 任何命令行输入或输出都编写如下: ```cpp > mkdir build && cd build > cmake .. > make recipe04_examples ``` **粗体**:表示一个新的术语、一个重要的单词或者你在屏幕上看到的单词。例如,重要的单词像这样出现在文本中。这里有一个例子:“在这个食谱中,我们将了解为什么在析构函数中抛出异常是一个**坏主意** Warnings or important notes appear like this. Tips and tricks appear like this. # 部分 在这本书里,你会发现几个经常出现的标题(*准备*,*怎么做...*、*它是如何工作的...*、*还有更多...*和*参见*。 要给出如何完成配方的明确说明,请使用以下章节: # 准备好 本节告诉您配方中的预期内容,并描述如何设置配方所需的任何软件或任何初步设置。 # 怎么做… 本节包含遵循配方所需的步骤。 # 它是如何工作的… 这一部分通常包括对前一部分发生的事情的详细解释。 # 还有更多… 本节包含关于配方的附加信息,以便您更好地了解配方。 # 请参见 本节提供了该配方的其他有用信息的有用链接。 # 取得联系 我们随时欢迎读者的反馈。 **一般反馈**:如果你对这本书的任何方面有疑问,在你的信息主题中提到书名,发邮件给我们`customercare@packtpub.com`。 **勘误表**:虽然我们已经尽了最大的努力来保证内容的准确性,但是错误还是会发生。如果你在这本书里发现了一个错误,如果你能向我们报告,我们将不胜感激。请访问[www.packtpub.com/support/errata](https://www.packtpub.com/support/errata),选择您的图书,点击勘误表提交链接,并输入详细信息。 **盗版**:如果您在互联网上遇到任何形式的我们作品的非法拷贝,如果您能提供我们的位置地址或网站名称,我们将不胜感激。请通过`copyright@packt.com`联系我们,并提供材料链接。 **如果你有兴趣成为一名作者**:如果有一个你有专长的话题,你有兴趣写或者投稿一本书,请访问[authors.packtpub.com](http://authors.packtpub.com/)。 # 复习 请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?然后,潜在的读者可以看到并使用您不带偏见的意见来做出购买决定,我们在 Packt 可以了解您对我们产品的看法,我们的作者可以看到您对他们的书的反馈。谢谢大家! 更多关于 Packt 的信息,请访问[packt.com](http://www.packt.com/)。 ================================================ FILE: docs/adv-cpp-prog-cb/01.md ================================================ # 一、库的开发入门 在本章中,我们将介绍一些创建自己的库的有用方法,包括对最小惊奇原则的解释,它鼓励我们使用用户已经熟悉的语义来实现库。我们还将研究如何命名一切,以确保我们的自定义库不会与其他库冲突。此外,我们将研究如何创建仅头库,以及一些与库开发相关的最佳实践。最后,我们将以 boost 库的演示来结束本章,向您展示大型库是什么样子的,以及用户如何在自己的项目中使用它。 在本章中,我们将介绍以下食谱: * 理解最少惊喜的原则 * 如何命名一切 * 仅标题库 * 学习库开发最佳实践 * 学习如何使用增强应用编程接口 我们开始吧! # 技术要求 要编译和运行本章中的示例,您必须拥有运行 Ubuntu 18.04 的计算机的管理权限,并且具有功能性互联网连接。在运行这些示例之前,您必须使用以下命令安装以下软件包: ```cpp > sudo apt-get install build-essential git cmake ``` 如果这安装在 Ubuntu 18.04 以外的任何操作系统上,则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。 # 理解最少惊喜的原则 无论是使用现有的 C++ 库还是创建自己的库,理解最少惊奇的**原则**(也称为最少惊奇的**原则**)对于高效和有效地开发源代码至关重要。这个原则简单地说明了 C++ 库提供的任何特性都应该是直观的,并且应该按照开发人员的期望运行。另一种说法是,库的 API 应该是自文档化的。虽然这个原则在设计库的时候非常重要,但是它可以并且应该应用于所有形式的软件开发。在这个食谱中,我们将深入探讨这个原理。 # 准备好 与本章中的所有方法一样,确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 执行以下步骤完成该配方: 1. 从新的终端,运行以下代码下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter01 ``` 2. 要编译源代码,请运行以下代码: ```cpp > mkdir build && cd build > cmake .. > make recipe01_examples ``` 3. 编译完源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe01_example01 The answer is: 42 > ./recipe01_example02 The answer is: 42 > ./recipe01_example03 The answer is: 42 > ./recipe01_example04 The answer is: 42 The answer is: 42 > ./recipe01_example05 The answer is: 42 The answer is: 42 > ./recipe01_example06 The answer is: 42 The answer is: 42 > ./recipe01_example07 The answer is: 42 > ./recipe01_example08 The answer is: 42 > ./recipe01_example09 The answer is: 42 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 如前一节所述,最少惊喜原则规定库的 API 应该是直观的和自我记录的,并且这个原则通常适用于所有形式的软件开发,而不仅仅是库设计。为了理解这一点,我们将看一些例子。 # 例 1 示例 1 演示了最小惊喜的原则,如下所示: ```cpp #include int sub(int a, int b) { return a + b; } int main(void) { std::cout << "The answer is: " << sub(41, 1) << '\n'; return 0; } ``` 如前面的例子所示,我们已经实现了一个将两个整数相加并返回结果的库 API。问题是我们把函数命名为`sub`,大多数开发者会联想到减法而不是加法;尽管 API 的功能与设计一样,但它打破了最不令人惊讶的原则,因为 API 的名称并不直观。 # 例 2 示例 2 演示了最少惊喜的原则,如下所示: ```cpp #include void add(int a, int &b) { b += a; } int main(void) { int a = 41, b = 1; add(a, b); std::cout << "The answer is: " << b << '\n'; return 0; } ``` 如前面的例子所示,我们已经实现了与我们在前面练习中实现的相同的库 API 它被设计为将两个数字相加并返回结果。这个例子的问题是应用编程接口正在实现以下内容: ```cpp b += a; ``` 在这个例子中,最少惊喜的原则以两种不同的方式被违反: * 加法函数的参数是`a`然后是`b`,即使我们将这个方程写成`b += a`,这意味着参数的顺序直观上是向后的。 * 对于这个应用编程接口的用户来说,在不阅读源代码的情况下,结果会在`b`中返回并不明显。 函数的签名应该使用用户已经习惯的语义来记录函数将如何执行,从而降低导致用户错误执行应用编程接口的概率。 # 例 3 示例 3 演示了最少惊喜的原则,如下所示: ```cpp #include int add(int a, int b) { return a + b; } int main(void) { std::cout << "The answer is: " << add(41, 1) << '\n'; return 0; } ``` 如前面的例子所示,我们在这里坚持最少惊喜的原则。API 的设计是将两个整数相加并返回结果,API 直观地按照预期执行这个动作。 # 例 4 示例 4 演示了最少惊喜的原则,如下所示: ```cpp #include #include int main(void) { printf("The answer is: %d\n", 42); std::cout << "The answer is: " << 42 << '\n'; return 0; } ``` 如前例所示,最小惊喜原则的另一个很好的例子是`printf()`和`std::cout`的区别。`printf()`函数需要添加格式说明符来输出整数到`stdout`。`printf()`不直观的原因有很多: * 对于初学者来说,`printf()`函数的名字代表打印格式,并不直观(或者换句话说,函数的名字不是自记录的)。其他语言通过为打印功能选择更直观的名称来避免这个问题,例如`print()`或`console()`,它们更好地坚持了最少惊喜的原则。 * 整数的格式说明符符号是`d`。同样,对于初学者来说,这是不直观的。在这种特殊情况下,`d`代表十进制,这是*有符号整数*的另一种说法。一个更好的格式说明符可能是`i`来匹配语言对`int`的使用。 对比一下`std::cout`,代表字符输出。虽然这与`print()`或`console()`相比不太直观,但比`printf()`更直观。此外,为了向`stdout`输出一个整数,用户不需要记住一个格式说明符表来完成他们的任务。相反,他们可以简单地使用`<<`运算符。然后,API 为您处理格式,这不仅更直观,而且更安全(尤其是在使用`std::cin`而不是`scanf()`时)。 # 例 5 示例 5 演示了最小惊喜的原则,如下所示: ```cpp #include int main(void) { auto answer = 41; std::cout << "The answer is: " << ++ answer << '\n'; std::cout << "The answer is: " << answer++ << '\n'; return 0; } ``` 如前例所示,`++ `操作者秉持最少惊喜的原则。虽然初学者必须学习`++ `代表增量运算符,这意味着变量由`1`增量,但是`++ `相对于变量的位置非常有帮助。 要理解`++ variable`和`variable++ `的区别,用户所要做的就是正常的从左到右读代码。当`++ `在左边时,变量递增,然后返回变量的内容。当`++ `在右边时,返回变量的内容,然后变量递增。关于`++ `位置的唯一问题是左边的`++ `通常更有效(因为在增量操作之前,实现不需要额外的逻辑来存储变量值)。 # 例 6 示例 6 展示了最少惊喜的原理如下: ```cpp #include int add(int a, int b) { return a + b; } int Sub(int a, int b) { return a - b; } int main(void) { std::cout << "The answer is: " << add(41, 1) << '\n'; std::cout << "The answer is: " << Sub(43, 1) << '\n'; return 0; } ``` 如前面的代码所示,我们已经实现了两个不同的 API。第一个将两个整数相加并返回结果,而第二个减去两个整数并返回结果。减法功能有两个问题: * 加法函数是小写的,而减法函数是大写的。这是不直观的,API 的用户必须了解哪些 API 是小写的,哪些是大写的。 * C++ 标准的 API 都是蛇的大小写,也就是说它们利用小写字母`_`来表示一个空格。总的来说,用 snake case 设计 C++ 库 API 更好,因为初学者更容易发现这种直观性。需要注意的是,虽然一般都是这样,但是使用 snake case 是非常主观的,有几种语言并不遵循这个指导。最重要的是选择一个惯例并坚持下去。 同样,确保您的应用编程接口模仿现有的语义可以确保用户可以快速轻松地学习使用您的应用编程接口,同时降低用户错误编写应用编程接口导致编译错误的可能性。 # 例 7 示例 7 展示了最小惊喜的原则,如下所示: ```cpp #include #include int main(void) { std::queue my_queue; my_queue.emplace(42); std::cout << "The answer is: " << my_queue.front() << '\n'; my_queue.pop(); return 0; } ``` 如前例所示,我们向您展示了如何使用`std::queue`向队列添加整数,将队列输出到`stdout`,并从队列中移除元素。这个例子的重点是强调这样一个事实,即 C++ 已经有了一套标准的命名约定,应该在 C++ 库开发过程中加以利用。 如果您正在设计一个新的库,使用与 C++ 已经定义的相同的命名约定对您的库的用户是有帮助的。这样做将降低进入门槛,并提供更直观的应用编程接口。 # 例 8 示例 8 展示了如下最少惊喜的原则: ```cpp #include auto add(int a, int b) { return a + b; } int main(void) { std::cout << "The answer is: " << add(41, 1) << '\n'; return 0; } ``` 如前面的例子所示,我们正在演示如何使用`auto`,它告诉编译器自动计算出函数的返回类型是什么,不坚持最少惊喜的原则。虽然`auto`对于编写泛型代码非常有帮助,但是在设计库 API 时应该尽可能避免使用它。具体来说,为了让应用编程接口的用户理解应用编程接口的输入和输出是什么,用户必须阅读应用编程接口的实现,因为`auto`没有指定输出类型。 # 例 9 示例 9 展示了最少惊喜的原则,如下所示: ```cpp #include template T add(T a, T b) { return a + b; } int main(void) { std::cout << "The answer is: " << add(41, 1) << '\n'; return 0; } ``` 如前面的例子所示,我们正在演示一种更合适的方法来支持最少惊奇的原则,同时支持泛型编程。泛型编程(也称为模板元编程或使用 C++ 模板编程)为程序员提供了一种创建算法的方法,而无需说明算法中使用的类型。在这种情况下,`add`函数不指定输入类型,允许用户添加任意类型的两个值(在这种情况下,类型称为`T`,它可以采用支持`add`运算符的任何类型)。我们返回一个类型`T`,而不是返回一个不会说明输出类型的`auto`。虽然`T`在这里没有定义,因为它代表任何类型,但它确实告诉应用编程接口的用户,我们输入到该函数中的任何类型也将由该函数返回。同样的逻辑在 C++ 标准库中大量使用。 # 如何命名一切 当创建一个库时,命名所有内容是很重要的。这样做可以确保库提供的 API 的名称与用户代码或其他库提供的工具冲突。在本食谱中,我们将展示如何在自己的库中做到这一点。 # 准备好 与本章中的所有方法一样,确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来完成此配方: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter01 ``` 2. 要编译源代码,请运行以下代码: ```cpp > mkdir build && cd build > cmake .. > make recipe02_examples ``` 3. 编译完源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe02_example01 The answer is: 42 > ./recipe02_example02 The answer is: 42 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... C++ 为我们提供了将代码包装在`namespace`中的能力,它只是将`namespace`的名称添加到`namespace`代码内部的所有函数和变量中(应该注意的是,C 风格的宏不包含在`namespace`中,应该小心使用,因为 C 宏是预处理器特性,不会对代码的编译语法产生影响)。为了解释为什么我们在创建自己的库时应该什么都用`namespace`,我们将看一些例子。 # 例 1 示例 1 演示了如何在 C++ 中包装库的应用编程接口: ```cpp // Contents of library.h namespace library_name { int my_api() { return 42; } // ... } // Contents of main.cpp #include int main(void) { using namespace library_name; std::cout << "The answer is: " << my_api() << '\n'; return 0; } ``` 如前面的例子所示,库的内容被包装在一个`namespace`中并存储在标题中(这个例子演示了一个只有标题的库,这是一个非常有用的设计方法,因为最终用户不必编译库,将它们安装在他/她的系统上,然后链接它们)。库用户只需包含库头文件,并使用`using namespace library_name`语句打开库的应用编程接口。如果用户有多个具有相同 API 名称的库,可以省略该语句以消除任何歧义。 # 例 2 示例 2 扩展了前面的示例,并演示了如何将您的库的 API 包装在一个 C++ 命名空间头文件库中,同时仍然包含全局变量: ```cpp // Contents of library.h namespace library_name { namespace details { inline int answer = 42; } int my_api() { return details::answer; } // ... } // Contents of main.cpp #include int main(void) { using namespace library_name; std::cout << "The answer is: " << my_api() << '\n'; return 0; } ``` 如前面的例子所示,C++ 17 被用来创建一个`inline`全局变量,该变量被包装在我们库的`namespace`中。`inline`变量是必需的,因为只有头文件的库没有定义全局变量的源文件;如果没有`inline`关键字,在头中定义一个全局变量将导致变量被多次定义(也就是说,结果将是编译期间的链接错误)。C++ 17 通过添加`inline`全局变量解决了这个问题,这允许一个只有头文件的库定义全局变量,而不需要复杂的魔法(比如从单例风格的函数返回一个静态变量的指针)。 除了库的`namespace`,我们将全局变量包装在一个`details namespace`中。这样做是为了在您的库中创建一个`private`位置,以防库的用户声明`using namespace library_name`。如果用户这样做,所有由`library_name`命名空间包装的 API 和变量在`main()`函数的范围内变得全局可访问。出于这个原因,任何不打算被用户访问的私有 API 或变量应该被第二个`namespace`(通常称为`details`)包装,以防止它们的全局可访问性。最后,利用 C++ 17 的`inline`关键字,我们可以创建一个全局变量,在我们的库中使用,同时仍然支持一个仅头设计。 # 仅标题库 只有头文件的库和它们听起来完全一样;整个库是使用头文件(通常是单个头文件)实现的。只包含头文件的库的好处是它们很容易包含在您的项目中,因为您只需包含头文件就完成了(不需要编译库,因为没有要编译的源文件)。在这个食谱中,我们将了解一些在试图创建一个只有标题的库时出现的问题以及如何克服它们。这个方法很重要,因为如果你计划创建自己的库,一个只有头文件的库是一个很好的开始,并且很可能会提高你的采用率,因为下游用户将很容易将你的库集成到他们的代码库中。 # 准备好 与本章中的所有方法一样,确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来完成此配方: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter01 ``` 2. 要编译源代码,请运行以下代码: ```cpp > mkdir build && cd build > cmake .. > make recipe03_examples ``` 3. 编译完源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe03_example01 The answer is: 42 > ./recipe03_example02 The answer is: 42 > ./recipe03_example03 The answer is: 42 > ./recipe03_example04 The answer is: 42 The answer is: 2a > ./recipe03_example05 > ./recipe03_example06 The answer is: 42 > ./recipe03_example07 The answer is: 42 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 要创建只包含头文件的库,只需确保所有代码都在头文件中实现,如下所示: ```cpp #ifndef MY_LIBRARY #define MY_LIBRARY namespace library_name { int my_api() { return 42; } } #endif ``` 前面的示例实现了一个具有单个函数的简单库。这个库的整个实现可以在一个头文件中实现,并包含在我们的代码中,如下所示: ```cpp #include "my_library.h" #include int main(void) { using namespace library_name; std::cout << "The answer is: " << my_api() << '\n'; return 0; } ``` 虽然创建仅头库看起来很简单,但是在尝试创建仅头库时会出现一些需要考虑的问题。 # 如何处理包括 在前面的示例中,您可能已经注意到,当我们使用自定义的仅头库时,我们首先包含了该库。这是编写纯头文件库必不可少的第一步。当为纯头文件库编写示例或测试时,我们的库应该是我们要包含的第一件事,以确保所有头文件的依赖项都是在纯头文件库中定义的,而不是在我们的示例或测试中。 例如,假设我们按如下方式更改库: ```cpp #ifndef MY_LIBRARY #define MY_LIBRARY namespace library_name { void my_api() { std::cout << "The answer is: 42" << '\n'; } } #endif ``` 如前面的代码片段所示,我们的 API 现在输出到`stdout`而不是返回一个整数。我们可以使用新的应用编程接口,如下所示: ```cpp #include #include "my_library.h" int main(void) { library_name::my_api(); return 0; } ``` 虽然前面的代码按预期编译和运行,但是代码中有一个错误,可能只有库的用户才能识别。具体来说,如果您的库的用户交换包含或不包含的顺序`#include `,代码将无法编译并产生以下错误: ![](img/14a94075-f946-458b-aa5c-a4bfd158978a.png) 这是因为纯头文件库本身并不包含它的所有依赖项。由于我们的示例将库放在其他包含之后,因此我们的示例意外地隐藏了这个问题。因此,在创建您自己的只包含标题的库时,一定要在您的测试和示例中首先包含该库,以确保这种类型的问题永远不会发生在您的用户身上。 # 全局变量 纯头文件库的最大限制之一是,在 C++ 17 之前,没有办法创建全局变量。尽管应该尽可能避免全局变量,但在某些情况下还是需要全局变量的。为了演示这一点,让我们创建一个简单的输出到`stdout`的应用编程接口,如下所示: ```cpp #ifndef MY_LIBRARY #define MY_LIBRARY #include #include namespace library_name { void my_api(bool show_hex = false) { if (show_hex) { std::cout << std::hex << "The answer is: " << 42 << '\n'; } else { std::cout << std::dec << "The answer is: " << 42 << '\n'; } } } #endif ``` 前面的例子创建了一个输出到`stdout`的应用编程接口。如果用`true`而不是默认的`false`执行该应用编程接口,它将以十六进制而不是十进制格式输出整数。在这个例子中,从十进制到十六进制的改变实际上是我们库中的一个配置设置。然而,如果没有全局变量,我们将不得不求助于其他机制来实现这一点,包括宏,或者在前面的例子中,函数参数;后一种选择更糟糕,因为它将库的配置与其 API 相耦合,这意味着任何额外的配置选项都会改变 API 本身。 解决这个问题的最好方法之一是在 C++ 17 中使用全局变量,如下所示: ```cpp #ifndef MY_LIBRARY #define MY_LIBRARY #include #include namespace library_name { namespace config { inline bool show_hex = false; } void my_api() { if (config::show_hex) { std::cout << std::hex << "The answer is: " << 42 << '\n'; } else { std::cout << std::dec << "The answer is: " << 42 << '\n'; } } } #endif ``` 如前面的例子所示,我们在库中添加了一个名为`config`的新名称空间。我们的应用编程接口不再需要任何参数,而是基于内联全局变量来确定如何运行。现在,我们可以如下使用这个应用编程接口: ```cpp #include "my_library.h" #include int main(void) { library_name::my_api(); library_name::config::show_hex = true; library_name::my_api(); return 0; } ``` 结果如下所示: ![](img/2abe9bca-2a0e-4075-a8f8-e5fa3140663e.png) 需要注意的是,我们将配置设置放在一个`config`命名空间中,以确保我们库的命名空间不会被名称冲突污染,这最终确保了全局变量的意图是显而易见的。 # C 风格宏的问题 C 风格宏的最大问题是,如果将它们放在 C++ 命名空间中,它们的名称不会被命名空间修饰。这意味着宏总是污染全局命名空间。例如,假设您正在编写一个需要检查变量值的库,如下所示: ```cpp #ifndef MY_LIBRARY #define MY_LIBRARY #include namespace library_name { #define CHECK(a) assert(a == 42) void my_api(int val) { CHECK(val); } } #endif ``` 如前面的代码片段所示,我们创建了一个简单的 API,它使用 C 风格的宏来检查其实现中的整数值。前面例子的问题是,如果您试图将单元测试库与您自己的库一起使用,您很可能会遇到名称空间冲突。 C++ 20 可以使用 C++ 20 模块来解决这个问题,这个话题我们将在[第 13 章](13.html)、*奖励-使用 C++ 20 特性*中详细讨论。具体来说,C++ 20 模块不向库的用户公开 C 风格的宏。积极的一面是,您将能够使用没有命名空间问题的宏,因为您的宏不会暴露给用户。这种方法的缺点是,许多库作者使用 C 风格的宏来配置库(例如,他们在包含库之前定义一个宏来更改其默认行为)。这种类型的库配置不适用于 C++ 模块,除非在编译库时在命令行上定义了宏。 在 C++ 20 可用之前,如果需要使用宏,请确保手动为宏名称添加装饰,如下所示: ```cpp #define LIBRARY_NAME__CHECK(a) assert(a == 42) ``` 前一行代码将执行与将宏放在 C++ 命名空间中相同的操作,确保您的宏不会与其他库中的宏或用户可能定义的宏冲突。 # 如何将大型库实现为仅头库 理想情况下,仅头库使用单个头来实现。也就是说,用户只需要将一个标题复制到他们的源代码中就可以使用这个库。这种方法的问题是,对于非常大的项目,单个标题可能会变得非常大。一个很好的例子是一个流行的 c++ JSON 库,位于这里:[https://github . com/nlohmann/JSON/blob/develop/single _ include/nlohmann/JSON . HPP](https://github.com/nlohmann/json/blob/develop/single_include/nlohmann/json.hpp)。 在撰写本文时,前面的库有 22,000 多行代码。试图修改一个有 22,000 行代码的文件是很糟糕的(如果你的编辑器能够处理的话)。一些项目通过使用几个头文件来实现它们的纯头文件库来克服这个问题,其中一个头文件根据需要包括各个头文件(例如,微软的 C++ 指南支持库就是这样实现的)。这种方法的问题在于,用户必须复制和维护多个头文件,随着头文件库的复杂性增加,这就开始违背了头文件库的目的。 处理这个问题的另一种方法是使用像 CMake 这样的东西从多个头文件中自动生成一个头文件。例如,在下面的示例中,我们有一个只有标题的库,其标题如下: ```cpp #include "config.h" namespace library_name { void my_api() { if (config::show_hex) { std::cout << std::hex << "The answer is: " << 42 << '\n'; } else { std::cout << std::dec << "The answer is: " << 42 << '\n'; } } } ``` 如前面的代码片段所示,这与我们的配置示例相同,只是示例的配置部分已被包含到`config.h`文件中的内容所替换。我们可以如下创建第二个头文件: ```cpp namespace library_name { namespace config { inline bool show_hex = false; } } ``` 这实现了示例的剩余部分。换句话说,我们已经把我们的标题分成了两个标题。我们仍然可以使用如下标题: ```cpp #include "apis.h" int main(void) { library_name::my_api(); return 0; } ``` 然而,问题是我们库的用户需要两个标题的副本。为了解决这个问题,我们需要自动生成一个头文件。有许多方法可以做到这一点,但以下是使用 CMake 的一种方法: ```cpp file(STRINGS "config.h" CONFIG_H) file(STRINGS "apis.h" APIS_H) list(APPEND MY_LIBRARY_SINGLE "${CONFIG_H}" "" "${APIS_H}" ) file(REMOVE "my_library_single.h") foreach(LINE IN LISTS MY_LIBRARY_SINGLE) if(LINE MATCHES "#include \"") file(APPEND "my_library_single.h" "// ${LINE}\n") else() file(APPEND "my_library_single.h" "${LINE}\n") endif() endforeach() ``` 前面的代码使用`file()`函数将两个头都读入到 CMake 变量中。这个函数将每个变量转换成一个字符串列表(每个字符串都是文件中的一行)。然后,我们将这两个文件合并成一个列表。为了创建新的、自动生成的单个头文件,我们遍历列表,并将每一行写到一个名为`my_library_single.h`的新头文件中。最后,如果我们看到对本地 include 的引用,我们会对其进行注释,以确保没有对附加头的引用。 现在,我们可以使用新的单头文件,如下所示: ```cpp #include "my_library_single.h" int main(void) { library_name::my_api(); return 0; } ``` 使用前面的方法,我们可以使用任意多的包含来开发我们的库,并且我们的构建系统可以自动生成我们的单个头文件,该文件将被最终用户使用,从而使我们两全其美。 # 学习库开发最佳实践 在编写自己的库时,所有库作者都应该遵循某些最佳实践。在本食谱中,我们将探索一些更高优先级的最佳实践,并以一些关于致力于定义这些最佳实践的项目的信息作为结束,包括一个注册系统,该系统为您的库提供了关于它编译得如何的评分。这个食谱很重要,因为它将教你如何制作最高质量的库,确保强大和充满活力的用户群。 # 准备好 与本章中的所有方法一样,确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake clang-tidy valgrind ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来完成此配方: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter01 ``` 2. 要编译源代码,请运行以下代码: ```cpp > mkdir build && cd build > cmake .. > make recipe04_examples ``` 3. 编译完源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe04_example01 21862 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 每个库作者都应该确保他们的库易于使用,并融入到用户自己的项目中。这样做将确保您的用户继续使用您的库,从而随着时间的推移增加用户群。让我们来看看其中的一些最佳实践。 # 警告呢? 对于任何一个库作者来说,最不可能的结果就是确保您的代码在编译时尽可能多地启用警告。可悲的是,GCC 并没有使这个过程变得简单,因为没有一个警告标志来统治它们,特别是因为 GCC 有许多警告标志对现代版本的 C++ 没有用(换句话说,在某种意义上,它们是互斥的)。最好从以下警告开始: ```cpp -Wall -Wextra -pedantic -Werror ``` 这将打开大多数重要的警告,同时确保您的示例或测试编译的任何警告都将生成错误。然而,对于一些库来说,这还不够。在撰写本文时,以下是微软指南支持库使用的标志: ```cpp -Wall -Wcast-align -Wconversion -Wctor-dtor-privacy -Werror -Wextra -Wpedantic -Wshadow -Wsign-conversion ``` GSL 使用的另一个警告是转换警告,当您在不同的整数类型之间转换时,它会告诉您。如果你使用的是 Clang,这个过程会容易很多,因为它提供了`-Weverything`。如果清除 GCC 提供的所有警告工作量太大,解决这个问题的一种方法是确保您的库在启用此警告的情况下用 Clang 编译器编译,这将确保您的代码在 GCC 提供的大多数警告下编译。这样,当您的用户必须确保在他们的代码中启用特定的警告时,他们就不会对您的库有任何问题,因为您已经测试了尽可能多的警告。 # 静态和动态分析 除了测试警告,还应该使用静态和动态分析工具测试库。同样,作为一个库的作者,您必须假设您的用户可能使用静态和动态分析工具来支持他们自己的应用的质量。如果你的库触发了这些工具,你的用户更有可能寻找已经被更彻底测试过的替代工具。 对于 C++,有大量的工具可以用来分析您的库。在这个食谱中,我们将重点介绍 Clang Tidy 和 Valgrind,这两个都是免费使用的。让我们看看下面这个简单的例子: ```cpp #include int universe() { auto i = new int; int the_answer; return the_answer; } int main() { std::cout << universe() << '\n'; return 0; } ``` 在前面的例子中,我们创建了一个名为`universe()`的函数,它返回一个整数并分配一个整数。在我们的主功能中,我们的`universe()`功能将结果输出到`stdout`。 为了静态地分析前面的代码,我们可以如下使用 CMake: ```cpp set(CMAKE_CXX_CLANG_TIDY clang-tidy) ``` 前一行代码告诉 CMake 在编译前一个例子时使用`clang-tidy`。当我们编译代码时,我们会得到以下结果: ![](img/90475cde-2dd5-45e5-a2fe-e2a0ed81b304.png) 如果您的库的用户已经使用 Clang Tidy 打开了静态分析,这是他们可能收到的错误,即使他们的代码非常好。如果您正在使用其他人的库,并遇到了这个问题,克服这个问题的一种方法是将库作为系统包含包括在内,这将告诉工具(如 Clang Tidy)忽略这些错误。然而,这并不总是有效的,因为有些库需要使用宏,这会将库的逻辑暴露给您自己的代码,从而导致混乱。一般来说,如果你是一个库开发人员,尽可能地静态分析你的库,因为你不知道你的用户会如何使用你的库。 动态分析也是如此。前面的分析没有检测到明显的内存泄漏。为了识别这一点,我们可以使用`valgrind`,如下所示: ![](img/e501ffa7-bfb7-46d1-b2e1-38630c9e0921.png) 如前面的截图所示,`valgrind`能够检测到我们代码中的内存泄漏。实际上,`valgrind`也检测到了这样一个事实,即我们从来没有在`universe()`函数中初始化我们的临时变量,但是输出过于冗长,无法在这里显示。同样,如果您不能识别您的库的这些类型的问题,您将最终向您的用户暴露这些错误。 # 证明文件 对于任何好的库来说,文档都是绝对必要的。除了有问题的代码,缺少文档绝对会阻止其他人使用你的库。库应该易于设置和安装,甚至更易于学习和整合到您自己的应用中。使用现有 C++ 库最令人沮丧的一个方面是缺少文档。 # CII 最佳实践 在这个食谱中,我们已经提到了一些所有库开发人员都应该纳入到他们的项目中的常见最佳实践。除了这些最佳实践,CII 最佳实践项目还提供了更完整的最佳实践列表:https://bestpractices.coreinfrastructure.org/en。 CII 最佳实践计划提供了一个全面的最佳实践列表,该列表会随着时间的推移而更新,库开发人员(以及一般的任何应用)都可以利用。这些最佳实践分为及格、银牌和金牌,金牌实践最难实现。您的分数越高,用户越有可能使用您的库,因为它显示了承诺和稳定性。 # 学习如何使用增强应用编程接口 增强库是一组旨在与标准 C++ 库协同工作的库。事实上,目前由 C++ 提供的许多库都源于 boost 库。增强库提供了从容器、时钟和计时器到更复杂的数学应用编程接口(如图形和循环冗余校验计算)的一切。在本食谱中,我们将学习如何使用增强库,特别是演示大型库是什么样子的,以及这样的库如何包含在用户的项目中。这个食谱很重要,因为它将展示一个库有多复杂,教你如何相应地编写自己的库。 # 准备好 与本章中的所有方法一样,确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake libboost-all-dev ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来完成此配方: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter01 ``` 2. 要编译源代码,请运行以下代码: ```cpp > mkdir build && cd build > cmake .. > make recipe05_examples ``` 3. 编译完源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe05_example01 Date/Time: 1553894555446451393 nanoseconds since Jan 1, 1970 > ./recipe05_example02 [2019-03-29 15:22:36.756819] [0x00007f5ee158b740] [debug] debug message [2019-03-29 15:22:36.756846] [0x00007f5ee158b740] [info] info message ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 增强库提供了一组用户 API,这些 API 实现了大多数程序中通常需要的功能。这些库可以包含在您自己的项目中,以简化您的代码,并提供一个成品库可能是什么样子的示例。为了解释你自己的库如何被其他人利用,让我们看一些如何使用增强库的例子。 # 例 1 在本例中,我们使用 boost APIs 将当前日期和时间输出到`stdout`,如下所示: ```cpp #include #include int main(void) { using namespace boost::chrono; std::cout << "Date/Time: " << system_clock::now() << '\n'; return 0; } ``` 如前例所示,当前日期和时间作为自 Unix 纪元(1970 年 1 月 1 日)以来的纳秒总数输出到`stdout`。除了在源代码中包含 boost 之外,还必须将应用与 boost 库进行链接。在这种情况下,我们需要针对以下内容进行链接: ```cpp -lboost_chrono -lboost_system -lpthread ``` 如何做到这一点的一个例子可以在这个食谱下载的`CMakeLists.txt`文件中看到。一旦这些库被链接到您的项目,您的代码将能够利用其中的 API。这一额外的步骤就是为什么只有标题的库在创建自己的库时如此有用,因为它们消除了额外链接的需要。 # 例 2 在本例中,我们演示了如何使用 boost 的简单日志记录 API 登录到控制台,如下所示: ```cpp #include int main(void) { BOOST_LOG_TRIVIAL(debug) << "debug message"; BOOST_LOG_TRIVIAL(info) << "info message"; return 0; } ``` 如前例所示,`"debug message"`和`"info message"`消息被输出到`stdout`。除了与适当的增强库链接之外,我们还必须在编译过程中包含以下定义: ```cpp -DBOOST_LOG_DYN_LINK -lboost_log -lboost_system -lpthread ``` 同样,链接这些库可以确保您在代码中使用的 API(如前面的示例所示)存在于可执行文件中。 # 请参见 有关增强库的更多信息,请查看[https://www.boost.org/](https://www.boost.org/)。 ================================================ FILE: docs/adv-cpp-prog-cb/02.md ================================================ # 二、将异常用于错误处理 在本章中,我们将学习一些高级的 C++ 异常处理技术。这里我们假设您对如何抛出和捕获 C++ 异常有基本的了解。本章将教您一些更高级的 C++ 异常处理技术,而不是专注于 C++ 异常的基础知识。这包括正确使用`noexcept`说明符和`noexcept`运算符,这样您就可以正确地将您的 APIs 标记为可能抛出异常或者明确地不抛出 C++ 异常,而不是在出现无法处理的错误时调用`std::terminate()`。 本章还将解释术语**资源获取是初始化** ( **RAII** )是什么,以及它如何补充 C++ 异常处理。我们还将讨论为什么不应该从类的析构函数中抛出 C++ 异常,以及如何处理这些类型的问题。最后,我们将看看如何创建您自己的自定义 C++ 异常,包括提供一些关于创建自己的异常时应该做什么和不应该做什么的基本指南。 从本章提供的信息中,您将更好地了解 C++ 异常是如何在幕后工作的,以及可以使用 C++ 异常来构建更健壮和可靠的 C++ 程序的类型。 本章中的配方如下: * 使用 noexcept 说明符 * 使用 noexcept 运算符 * 使用 RAII * 学习为什么不在析构函数中抛出异常 * 轻松创建自己的异常类 # 技术要求 要编译和运行本章中的示例,您必须拥有运行 Ubuntu 18.04 的计算机的管理权限,并且具有功能性互联网连接。在运行这些示例之前,您必须安装以下内容: ```cpp sudo apt-get install build-essential git cmake ``` 如果这安装在 Ubuntu 18.04 以外的任何操作系统上,则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。 # 使用 noexcept 说明符 `noexcept`说明符用于告诉编译器一个函数是否可以抛出 C++ 异常。如果函数标有`noexcept`说明符,则不允许引发异常,如果是,则在引发异常时将调用`std::terminate()`。如果函数没有`noexcept`说明符,可以正常抛出异常。 在本食谱中,我们将探索如何在您自己的代码中使用`noexcept`说明符。这个说明符很重要,因为它是您正在创建的应用编程接口和应用编程接口用户之间的契约。当使用`noexcept`说明符时,它告诉应用编程接口的用户在使用应用编程接口时不需要考虑异常。它还告诉作者,如果他们将`noexcept`说明符添加到他们的 API 中,他们必须确保不抛出任何异常,在某些情况下,这要求作者捕获所有可能的异常,如果异常无法处理,要么处理它们,要么调用`std::terminate()`。此外,还有某些操作,例如`std::move`,在这些操作中,如果不担心损坏,就不能抛出异常,因为如果抛出异常,移动操作通常不能安全地反转。最后,对于一些编译器来说,在您的 API 中添加`noexcept`将减少函数的整体大小,从而导致整体应用更小。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 要尝试此配方,请执行以下步骤: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter02 ``` 2. 要编译源代码,请运行以下命令: ```cpp > mkdir build && cd build > cmake .. > make recipe01_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe01_example01 The answer is: 42 > ./recipe01_example02 terminate called after throwing an instance of 'std::runtime_error' what(): The answer is: 42 Aborted > ./recipe01_example03 The answer is: 42 > ./recipe01_example04 terminate called after throwing an instance of 'std::runtime_error' what(): The answer is: 42 Aborted > ./recipe01_example05 foo: 18446744069414584320 foo: T is too large ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 首先,让我们简单回顾一下 C++ 异常是如何抛出和捕获的。在下面的例子中,我们将从一个函数抛出一个异常,然后在我们的`main()`函数中捕获该异常: ```cpp #include #include void foo() { throw std::runtime_error("The answer is: 42"); } int main(void) { try { foo(); } catch(const std::exception &e) { std::cout << e.what() << '\n'; } return 0; } ``` 如前面的例子所示,我们创建了一个名为`foo()`的函数,它抛出了一个异常。这个函数在我们的`try` / `catch`块内的`main()`函数中被调用,该函数用于捕捉在`try`块内执行的代码可能抛出的任何异常,在本例中,该块是`foo()`函数。当`foo()`功能抛出异常时,成功捕获并输出到`stdout`。 所有这些都可以工作,因为我们没有给`foo()`函数添加`noexcept`说明符。默认情况下,一个函数被允许抛出一个异常,就像我们在这个例子中做的那样。然而,在某些情况下,我们不希望抛出异常,这取决于我们期望函数如何执行。具体来说,函数如何处理异常可以定义为以下内容(称为异常安全): * **不抛出保证**:函数不能抛出异常,如果在内部抛出异常,必须捕捉并处理异常,包括分配失败。 * **强异常安全**:函数可以抛出异常,如果抛出异常,任何被函数修改的状态都会回滚或者撤销,没有任何副作用。 * **基本异常安全**:函数可以抛出异常,如果抛出异常,任何被函数修改过的状态都会回滚或者撤销,但是有可能产生副作用。应该注意的是,这些副作用不包括不变量,这意味着程序处于有效的、未损坏的状态。 * **无异常安全**:函数可以抛出异常,如果抛出异常,程序可能会进入损坏状态。 一般来说,如果一个函数有不抛出的保证,就用`noexcept`标注;否则,就不是了。异常安全如此重要的一个例子是`std::move`。例如,假设我们有两个`std::vector`的例子,我们希望将一个向量移动到另一个向量中。要执行移动,`std::vector`可能会将向量的每个元素从一个实例移动到另一个实例。如果对象在移动时被允许抛出,那么向量可能会在移动过程中出现异常(也就是说,向量中一半的对象被成功移动)。当异常发生时,`std::vector`显然会尝试在返回异常之前,通过将这些动作移回原始向量来撤销已经执行的动作。问题是,试图将对象移回需要`std::move()`,这可能会再次抛出异常,导致嵌套异常。在实践中,将一个`std::vector`实例移动到另一个实例实际上并不会执行逐对象移动,但是调整大小会执行,在这个特定的问题中,标准库需要使用`std::move_if_noexcept`来处理这种情况,以提供异常安全,当允许对象的移动构造函数抛出时,这又会返回到副本。 `noexcept`说明符通过明确声明函数不允许抛出异常来克服这些类型的问题。这不仅告诉应用编程接口的用户,他们可以安全地使用该函数,而不必担心抛出异常并可能破坏程序的执行,而且还迫使函数的作者安全地处理所有可能的异常或调用`std::terminate()`。虽然`noexcept`依赖于编译器,也通过在定义时减少应用的整体大小来提供优化,但它的主要用途是陈述函数的异常安全性,以便其他函数可以推断函数将如何执行。 在下面的例子中,我们将`noexcept`说明符添加到前面定义的`foo()`函数中: ```cpp #include #include void foo() noexcept { throw std::runtime_error("The answer is: 42"); } int main(void) { try { foo(); } catch(const std::exception &e) { std::cout << e.what() << '\n'; } return 0; } ``` 当这个例子被编译和执行时,我们得到如下结果: ![](img/f99d2218-74b5-47f1-8108-6a38646732a8.png) 如前例所示,添加了`noexcept`说明符,告知编译器不允许`foo()`抛出异常。然而,由于`foo()`函数确实抛出了一个异常,所以当它被执行时,会调用`std::terminate()`。事实上,在这个例子中,`std::terminate()`将总是被调用,这是编译器能够检测和警告的事情。 调用`std::terminate()`显然不是一个程序想要的结果。在这种特定的情况下,由于作者已经将该功能标记为`noexcept`,因此由作者来处理所有可能的异常。这可以通过以下方式实现: ```cpp #include #include void foo() noexcept { try { throw std::runtime_error("The answer is: 42"); } catch(const std::exception &e) { std::cout << e.what() << '\n'; } } int main(void) { foo(); return 0; } ``` 如上例所示,异常被包装在`try` / `catch`块中,以确保在`foo()`函数完成其执行之前安全地处理异常。此外,在本例中,仅捕获源自`std::exception()`的异常。这是作者说哪些类型的异常可以安全处理的方式。例如,如果抛出一个整数而不是`std::exception()`,`std::terminate()`仍然会自动执行,因为`noexcept`被添加到了`foo()`函数中。换句话说,作为作者,您只需要处理您实际上可以安全处理的异常。剩下的会为你送到`std::terminate()`;请理解,通过这样做,您改变了函数的异常安全性。如果您打算用不抛出保证来定义函数,那么该函数根本不会抛出异常。 还需要注意的是,如果将一个函数标记为`noexcept`,不仅需要注意自己抛出的异常,还需要注意可能自己抛出的函数。在这种情况下,`std::cout`在`foo()`函数中使用,这意味着作者必须要么故意忽略`std::cout`可能抛出的任何异常,这将导致对`std::terminate()`的调用(这就是我们在这里所做的),要么作者需要确定`std::cout`可能抛出哪些异常并尝试安全地处理它们,包括`std::bad_alloc`之类的异常。 如果所提供的索引超出向量的界限,则`std::vector.at()`函数抛出`std::out_of_range()`异常。在这种情况下,作者可以捕捉这种类型的异常并返回一个默认值,允许作者安全地将该函数标记为`noexcept`。 `noexcept`说明符也可以作为一个函数,采用布尔表达式,如下例所示: ```cpp #include #include void foo() noexcept(true) { throw std::runtime_error("The answer is: 42"); } int main(void) { try { foo(); } catch(const std::exception &e) { std::cout << e.what() << '\n'; } return 0; } ``` 执行时会产生以下结果: ![](img/c75fc0ac-3445-4fe6-a20c-934b783a5d96.png) 如前例所示,`noexcept`说明符写成了`noexcept(true)`。如果表达式评估为真,则好像提供了`noexcept`。如果表达式的计算结果为假,就好像省略了`noexcept`说明符,允许抛出异常。在前面的例子中,表达式的计算结果为 true,这意味着函数不允许抛出异常,这导致在`foo()`抛出异常时调用`std::terminate()`。 让我们看一个更复杂的例子来演示如何使用它。在下面的例子中,我们将创建一个名为`foo()`的函数,该函数将整数值移位 32 位,并将结果转换为 64 位整数。这个例子将使用模板元编程编写,允许我们在任何整数类型上使用这个函数: ```cpp #include #include #include template uint64_t foo(T val) noexcept(sizeof(T) <= 4) { if constexpr(sizeof(T) <= 4) { return static_cast(val) << 32; } throw std::runtime_error("T is too large"); } int main(void) { try { uint32_t val1 = std::numeric_limits::max(); std::cout << "foo: " << foo(val1) << '\n'; uint64_t val2 = std::numeric_limits::max(); std::cout << "foo: " << foo(val2) << '\n'; } catch(const std::exception &e) { std::cout << e.what() << '\n'; } return 0; } ``` 执行时会产生以下结果: ![](img/182cbe31-a769-4160-884a-7f9445e380d2.png) 如前例所示,`foo()`函数的问题是,如果用户提供 64 位整数,它不能移位 32 位而不产生溢出。然而,如果提供的整数是 32 位或更少,则`foo()`功能是完全安全的。为了实现`foo()`函数,我们使用了`noexcept`说明符来声明如果提供的整数是 32 位或更少,则不允许该函数抛出异常。如果提供的整数大于 32 位,则允许抛出异常,在这种情况下,这是一个`std::runtime_error()`异常,表示整数太大,无法安全移位。 # 使用 noexcept 运算符 `noexcept`运算符是编译时检查,用于询问编译器某个函数是否被标记为`noexcept`。在 C++ 17 中,这可以与编译时的`if`语句(即在编译时评估的`if`语句,可用于在编译期间从可执行文件中添加/删除代码)配对,以根据是否允许函数引发异常来更改程序的语义。 在本食谱中,我们将探索如何在您自己的代码中使用`noexcept`运算符。这个运算符很重要,因为在某些情况下,您可能不知道一个函数是否能够通过简单地查看其定义来引发异常。例如,如果函数使用`noexcept`说明符,您的代码可能无法确定函数是否会抛出,因为您可能不知道(基于函数的输入)`noexcept`说明符的计算结果。`noexcept`操作符为您提供了处理这些类型场景的机制,这是必不可少的,尤其是在元编程时。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 执行以下步骤来尝试配方: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter02 ``` 2. 要编译源代码,请运行以下命令: ```cpp > mkdir build && cd build > cmake .. > make recipe02_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe02_example01 could foo throw: true > ./recipe02_example02 could foo throw: true could foo throw: true could foo throw: false could foo throw: false > ./recipe02_example03 terminate called after throwing an instance of 'std::runtime_error' what(): The answer is: 42 Aborted > ./recipe02_example04 > ./recipe02_example05 terminate called after throwing an instance of 'std::runtime_error' what(): The answer is: 42 Aborted > ./recipe02_example06 could foo throw: true could foo throw: true ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... `noexcept`运算符用于确定一个函数是否可以抛出。让我们从一个简单的例子开始: ```cpp #include #include void foo() { std::cout << "The answer is: 42\n"; } int main(void) { std::cout << std::boolalpha; std::cout << "could foo throw: " << !noexcept(foo()) << '\n'; return 0; } ``` 这将导致以下结果: ![](img/afafa314-071a-4aa9-8896-0c19d3282f99.png) 如前例所示,我们定义了一个输出到`stdout`的`foo()`函数。我们实际上并不执行`foo()`,而是使用`noexcept`运算符来检查`foo()`函数是否可以抛出。如你所见,答案是肯定的;这个函数可以抛出。这是因为我们没有用`noexcept`来标记`foo()`函数,并且,如前一个配方中所述,默认情况下函数可以抛出。 还需要注意的是,我们在`noexcept`表达式中加入了`!`。这是因为如果函数被标记为`noexcept`,则`noexcept`返回`true`,这意味着该函数不允许抛出。然而,在我们的例子中,我们不是问函数是否不能抛出,而是问函数是否能抛出,因此逻辑布尔反转。 让我们通过在我们的示例中添加几个函数来对此进行扩展。具体来说,在下面的例子中,我们将添加一些抛出的函数以及一些标记为`noexcept`的函数: ```cpp #include #include void foo1() { std::cout << "The answer is: 42\n"; } void foo2() { throw std::runtime_error("The answer is: 42"); } void foo3() noexcept { std::cout << "The answer is: 42\n"; } void foo4() noexcept { throw std::runtime_error("The answer is: 42"); } int main(void) { std::cout << std::boolalpha; std::cout << "could foo throw: " << !noexcept(foo1()) << '\n'; std::cout << "could foo throw: " << !noexcept(foo2()) << '\n'; std::cout << "could foo throw: " << !noexcept(foo3()) << '\n'; std::cout << "could foo throw: " << !noexcept(foo4()) << '\n'; return 0; } ``` 这将导致以下结果: ![](img/6c634422-311e-40ae-a7f8-e20aa940f7a4.png) 如前例所示,如果一个函数标有`noexcept`,`noexcept`运算符返回`true`(在我们的示例中,它输出`false`)。更重要的是,敏锐的观察者会注意到抛出异常的函数不会改变`noexcept`运算符的输出。也就是说,如果某个功能*可以*抛出异常,则`noexcept`操作符返回`false`,否则*会*抛出异常。这很重要,因为知道函数*是否会*抛出异常的唯一方法是执行它。`noexcept`说明符唯一声明的是函数是否允许抛出异常。没有说明是否会抛出异常*。推而广之,`noexcept`运算符不会告诉您函数*是否会*抛出,而是告诉您该函数是否标有`noexcept`说明符(更重要的是,`noexcept`说明符的计算结果)。* *在我们尝试在一个更现实的例子中使用`noexcept`说明符之前,让我们看一下下面的例子: ```cpp #include #include void foo() { throw std::runtime_error("The answer is: 42"); } int main(void) { foo(); } ``` 如前面的例子所示,我们已经定义了一个抛出的`foo()`函数,然后我们从我们的主函数中调用这个函数,导致`std::terminate()`被调用,因为我们在离开程序之前没有处理异常。在更复杂的设置中,我们可能不知道`foo()`是否抛出,因此,如果不需要的话,我们可能不想增加额外的异常处理开销。为了更好地解释这一点,让我们检查这个例子的`main()`函数的结果汇编代码: ![](img/8741e7cf-194c-44e7-84c5-b48af8c04011.png) 可以看到,`main`函数很简单,除了调用`foo`函数之外,不包含任何额外的逻辑。具体来说,`main`函数没有任何捕捉逻辑。 现在,让我们在一个更具体的例子中使用`noexcept`运算符: ```cpp #include #include void foo() { throw std::runtime_error("The answer is: 42"); } int main(void) { if constexpr(noexcept(foo())) { foo(); } else { try { foo(); } catch (...) { } } } ``` 如上例所示,在 C++ 17 中添加的`if`语句中,我们将`noexcept`运算符与`constepxr`运算符结合使用。这让我们可以问编译器`foo()`是否允许抛出。如果是,我们在`try` / `catch`块中执行`foo()`函数,这样我们就可以根据需要处理任何可能的异常。如果我们检查这个函数的程序集,如下面的截图所示,我们可以看到一些额外的`catch`逻辑被添加到生成的二进制文件中,以根据需要处理异常: ![](img/49f6bfba-8bae-40ba-8987-e352f7b9625c.png) 现在,让我们通过声明`foo()`函数不允许使用`noexcept`说明符来抛出,从而将同一个示例向前推进一步: ```cpp #include #include void foo() noexcept { throw std::runtime_error("The answer is: 42"); } int main(void) { if constexpr(noexcept(foo())) { foo(); } else { try { foo(); } catch (...) { } } } ``` 如前例所示,由于`foo()`函数被标记为`noexcept`,程序调用`std::terminate()`。此外,如果我们查看最终的装配,我们可以看到`main()`功能不再包含额外的`try` / `catch`逻辑,这意味着我们的优化成功了: ![](img/2d0478f9-51e3-4438-b303-7d4872bf5a80.png) 最后,如果我们不知道被调用的函数是否可以抛出,我们可能不知道如何标记自己的函数。让我们看下面的例子来说明这个问题: ```cpp #include #include void foo1() { std::cout << "The answer is: 42\n"; } void foo2() noexcept(noexcept(foo1())) { foo1(); } int main(void) { std::cout << std::boolalpha; std::cout << "could foo throw: " << !noexcept(foo1()) << '\n'; std::cout << "could foo throw: " << !noexcept(foo2()) << '\n'; } ``` 这将导致以下结果: ![](img/5c98505b-2992-4bc6-a927-e4eb3315fd00.png) 如前例所示,`foo1()`函数没有标注`noexcept`说明符,这意味着允许它抛出异常。在`foo2()`中,我们想要确保我们的`noexcept`说明符是正确的,但是我们称之为`foo1()`,并且在这个例子中,我们假设我们不知道`foo1()`是否是`noexcept`。 为了确保`foo2()`被正确标记,我们结合本食谱和上一份食谱中的经验教训来正确标记功能。具体来说,我们使用`noexcept`运算符来告诉我们`foo1()`函数是否会抛出,然后我们使用`noexcept`说明符的布尔表达式语法来使用`noexcept`运算符的结果来标记`foo2()`是否为`noexcept`。如果`foo1()`标有`noexcept`,`noexcept`操作者将返回`true`,导致`foo2()`被标为`noexcept(true)`,与简单的说明`noexcept`相同。如果`foo1()`没有标记为`noexcept`,则`noexcept`运算符将返回`false`,此时`noexcept`说明符将标记为`noexcept(false)`,这与不添加`noexcept`说明符(即允许函数抛出异常)是一样的。 # 使用 RAII RAII 是一种编程原则,它声明资源与获取资源的对象的生存期相关联。RAII 是 C++ 语言的一个强大特性,它确实有助于将 C++ 与 C 区分开来,有助于防止资源泄漏和一般的不稳定性。 在这个食谱中,我们将深入研究 RAII 是如何工作的,以及如何使用 RAII 来确保 C++ 异常不会导致资源泄漏。RAII 对于任何 C++ 应用来说都是一项关键技术,应该尽可能使用。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来尝试配方: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter02 ``` 2. 要编译源代码,请运行以下命令: ```cpp > mkdir build && cd build > cmake .. > make recipe03_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe03_example01 The answer is: 42 > ./recipe03_example02 The answer is: 42 > ./recipe03_example03 The answer is not: 43 > ./recipe03_example04 The answer is: 42 > ./recipe03_example05 step 1: Collect answers The answer is: 42 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 为了更好地理解 RAII 是如何工作的,我们必须首先研究 C++ 中的类是如何工作的,因为 C++ 类是用来实现 RAII 的。让我们看一个简单的例子。C++ 类同时支持构造函数和析构函数,如下所示: ```cpp #include #include class the_answer { public: the_answer() { std::cout << "The answer is: "; } ~the_answer() { std::cout << "42\n"; } }; int main(void) { the_answer is; return 0; } ``` 在编译和执行时,这会产生以下结果: ![](img/1991efef-e0b0-48f0-9c36-1a62bfbec715.png) 在前面的例子中,我们用构造函数和析构函数创建了一个类。当我们创建类的实例时,调用构造函数,当类的实例失去作用域时,类被销毁。这是一个简单的 C++ 模式,自从 C++ 的最初版本由比雅尼·斯特劳斯特鲁普创建以来就一直存在。在幕后,编译器在类第一次实例化时调用构造函数,但更重要的是,当类的实例化失去作用域时,编译器必须向执行销毁函数的程序中注入代码。这里需要理解的重要一点是,这个附加逻辑是由程序员的编译器自动插入到程序中的。 在引入类之前,程序员必须手动向程序添加构造和销毁逻辑,虽然构造是一件相当简单的事情,但销毁却不是。C 语言中这类问题的一个典型例子是存储文件句柄。程序员将添加对`open()`函数的调用以打开文件句柄,当文件完成时,将添加对`close()`的调用以关闭文件句柄,忘记对所有可能出现的错误情况执行`close()`函数。这包括当代码长达数百行,程序中的新成员添加了另一个错误案例时,忘记根据需要调用`close()`。 RAII 通过确保一旦类失去作用域,不管控制流路径是什么,获取的资源都会被释放,从而解决了这个问题。让我们看看下面的例子: ```cpp #include #include class the_answer { public: int *answer{}; the_answer() : answer{new int} { *answer = 42; } ~the_answer() { std::cout << "The answer is: " << *answer << '\n'; delete answer; } }; int main(void) { the_answer is; if (*is.answer == 42) { return 0; } return 1; } ``` 在这个例子中,我们分配一个整数,并在类的构造函数中初始化它。这里需要注意的重要一点是,我们不需要从`new`操作员那里检查`nullptr`。这是因为如果内存分配失败,`new`运算符将抛出异常。如果发生这种情况,不仅构造函数的其余部分不会被执行,而且对象本身也不会被构造。这意味着如果构造函数成功执行,您就知道该类的实例处于有效状态,并且实际上包含一个资源,当该类的实例失去作用域时,该资源将被销毁 该类的析构函数然后输出到`stdout`并删除先前分配的内存。这里需要理解的重要一点是,无论代码采用什么控制路径,当类的实例失去作用域时,这个资源都会被释放。程序员只需要担心类的寿命。 资源的生命周期与分配资源的对象的生命周期直接相关,这一思想很重要,因为它解决了存在 C++ 异常时程序控制流的复杂问题。让我们看看下面的例子: ```cpp #include #include class the_answer { public: int *answer{}; the_answer() : answer{new int} { *answer = 43; } ~the_answer() { std::cout << "The answer is not: " << *answer << '\n'; delete answer; } }; void foo() { the_answer is; if (*is.answer == 42) { return; } throw std::runtime_error(""); } int main(void) { try { foo(); } catch(...) { } return 0; } ``` 在这个例子中,我们创建了与前一个例子相同的类,但是,在我们的`foo()`函数中,我们抛出了一个异常。但是`foo()`函数不需要捕捉这个异常来确保分配的内存被正确释放。相反,析构函数为我们处理这个。在 C++ 中,许多函数可能会抛出,如果没有 RAII,每一个可能抛出的函数都需要包装在一个`try` / `catch`块中,以确保分配的任何资源都被正确释放。事实上,我们在 C 代码中经常看到这种模式,尤其是在内核级编程中,使用`goto`语句来确保在一个函数中,如果发生错误,该函数可以适当地展开自己,以释放之前可能获得的任何资源。这个结果是一组代码,用于检查程序中每个函数调用的结果以及正确处理错误所需的逻辑。 有了这种类型的编程模型,难怪资源泄漏在 C 中如此常见。RAII 结合 C++ 异常消除了对这种容易出错的逻辑的需求,导致代码不太可能泄漏资源。 在存在 C++ 异常的情况下如何处理 RAII 不在本书的讨论范围之内,因为它需要更深入地研究 C++ 异常支持是如何实现的。需要记住的重要一点是,C++ 异常比检查函数的返回值是否有错误更快(因为 C++ 异常是使用无开销算法实现的),但是当抛出实际的异常时会很慢(因为程序必须展开堆栈并根据需要正确执行每个类的析构函数)。由于这个原因,以及诸如可维护性等其他原因,C++ 异常永远不应该用于有效的控制流。 RAII 可以使用的另一种方式是`finally`模式,由 C++ **指南支持库** ( **GSL** )提供。`finally`模式利用 RAI 中只包含析构函数的部分,当函数的控制流复杂或可能抛出时,提供一种简单的机制来执行非基于资源的清理。考虑以下示例: ```cpp #include #include template class finally { FUNC m_func; public: finally(FUNC func) : m_func{func} { } ~finally() { m_func(); } }; int main(void) { auto execute_on_exit = finally{[]{ std::cout << "The answer is: 42\n"; }}; } ``` 在前面的例子中,我们创建了一个能够存储 lambda 函数的类,当`finally`类的一个实例失去作用域时执行该函数。在这种特殊情况下,当`finally`类被破坏时,我们输出到`stdout`。虽然这使用了类似于 RAII 的模式,但在技术上这不是 RAII,因为没有获得任何资源。 另外,如果确实需要获取资源,应该使用 RAII 而不是`finally`模式。相反,`finally`模式在您没有获取资源但想要在函数返回时执行代码时很有用,不管程序采取什么控制流路径(条件分支或 C++ 异常)。 为了演示这一点,让我们看一个更复杂的例子: ```cpp #include #include template class finally { FUNC m_func; public: finally(FUNC func) : m_func{func} { } ~finally() { m_func(); } }; int main(void) { try { auto execute_on_exit = finally{[]{ std::cout << "The answer is: 42\n"; }}; std::cout << "step 1: Collect answers\n"; throw std::runtime_error("???"); std::cout << "step 3: Profit\n"; } catch (...) { } } ``` 执行时,我们会得到以下结果: ![](img/974fa02a-a5bd-462b-aa43-3951b03d15dc.png) 在前面的例子中,我们希望确保无论代码做什么,我们总是输出到`stdout`。在执行过程中,我们抛出了一个异常,即使抛出了异常,我们的`finally`代码也是按预期执行的。 # 学习为什么不在析构函数中抛出异常 在这个食谱中,我们将讨论 C++ 异常的问题,特别是在类析构函数中抛出异常,这是应该不惜一切代价避免的。这个食谱中的经验很重要,因为与其他函数不同,C++ 类析构函数在默认情况下被标记为`noexcept`,这意味着如果你不小心在类析构函数中抛出了一个异常,你的程序将调用`std::terminate()`,即使析构函数没有被公开标记为`noexcept`。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 执行以下步骤来尝试配方: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter02 ``` 2. 要编译源代码,请运行以下命令: ```cpp > mkdir build && cd build > cmake .. > make recipe04_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe04_example01 terminate called after throwing an instance of 'std::runtime_error' what(): 42 Aborted > ./recipe04_example02 The answer is: 42 > ./recipe04_example03 terminate called after throwing an instance of 'std::runtime_error' what(): 42 Aborted > ./recipe04_example04 # exceptions: 2 The answer is: 42 The answer is: always 42 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 在这个食谱中,我们将了解为什么在析构函数中抛出异常是一个糟糕的*想法,以及为什么类析构函数被默认标记为`noexcept`。首先,让我们看一个简单的例子:* ```cpp #include #include class the_answer { public: ~the_answer() { throw std::runtime_error("42"); } }; int main(void) { try { the_answer is; } catch (const std::exception &e) { std::cout << "The answer is: " << e.what() << '\n'; } } ``` 当我们执行此操作时,我们会得到以下结果: ![](img/3d30c668-41a6-430a-8fc4-a95a1da1f660.png) 在这个例子中,我们可以看到,如果我们从类析构函数抛出一个异常,就会调用`std::terminate()`。这是因为,默认情况下,类析构函数被标记为`noexcept`。 我们可以通过将类的析构函数标记为`noexcept(false)`来显式允许类的析构函数抛出来改变这一点,如下例所示: ```cpp #include #include class the_answer { public: ~the_answer() noexcept(false) { throw std::runtime_error("42"); } }; int main(void) { try { the_answer is; } catch (const std::exception &e) { std::cout << "The answer is: " << e.what() << '\n'; } } ``` 如前面的示例所示,当类被销毁时,会引发异常并得到正确处理。即使成功处理了这个问题,我们也要问自己,在我们捕捉到这个异常后,程序的状态是什么?析构函数没有成功完成。如果这个类更复杂,并且有它管理的状态/资源,我们能断定我们关心的状态/资源被正确处理/释放了吗?简短的回答是否定的,这和用锤子破坏硬盘是一样的。如果你用锤子猛击硬盘来破坏它,你真的破坏了硬盘上的数据吗?没有办法知道,因为当你用锤子敲击硬盘时,你打碎了用来回答这个问题的电子设备。当您试图销毁硬盘驱动器时,您需要一个可靠的过程来确保在任何情况下销毁驱动器的过程都不会使数据处于可恢复状态。否则,你没有办法知道自己处于什么状态,没有办法回去。 这同样适用于 C++ 类。销毁一个 C++ 类需要是一个必须提供基本异常安全的操作(也就是说,程序的状态是确定性的,有一些可能的副作用)。否则,唯一的另一个逻辑行动就是调用`std::terminate()`,因为你无法确定如果程序继续执行会发生什么。 除了将程序置于未定义状态之外,从析构函数中抛出异常的另一个问题是,如果已经抛出了异常,会发生什么?`try` / `catch`区块捕捉到了什么?让我们看一个这类问题的例子: ```cpp #include #include class the_answer { public: ~the_answer() noexcept(false) { throw std::runtime_error("42"); } }; int main(void) { try { the_answer is; throw std::runtime_error("first exception"); } catch (const std::exception &e) { std::cout << "The answer is: " << e.what() << '\n'; } } ``` 在前面的例子中,我们将析构函数标记为`noexcept(false)`,就像我们在前面的例子中所做的那样,但是我们在调用析构函数之前抛出,这意味着,当调用析构函数时,已经有一个异常正在被处理。现在,当我们试图抛出时,虽然析构函数被标记为`noexcept(false)`,但是`std::terminate()`被调用: ![](img/afeb2214-83a3-4320-bc7d-abea44492169.png) 原因是 C++ 库没有办法处理这种情况,因为`try` / `catch`块不能处理多个异常。然而,可能有一个以上的未决例外;我们只需要一个`try` / `catch`块来处理每个异常。当我们有嵌套异常时,就会出现这种情况,如本例所示: ```cpp #include #include class nested { public: ~nested() { std::cout << "# exceptions: " << std::uncaught_exceptions() << '\n'; } }; class the_answer { public: ~the_answer() { try { nested n; throw std::runtime_error("42"); } catch (const std::exception &e) { std::cout << "The answer is: " << e.what() << '\n'; } } }; ``` 在本例中,我们将从创建一个输出调用`std::uncaught_exceptions()`结果的类开始,该类返回当前正在处理的异常总数。然后,我们将创建第二个类,该类创建第一个类,然后从其析构函数中抛出,需要注意的是,析构函数中的所有代码都被包装在一个`try` / `catch`块中: ```cpp int main(void) { try { the_answer is; throw std::runtime_error("always 42"); } catch (const std::exception &e) { std::cout << "The answer is: " << e.what() << '\n'; } } ``` 执行此示例时,我们会得到以下结果: ![](img/afcfefea-5caa-45ba-8cb6-817eb3023c2f.png) 最后,我们将创建这个第二类,并用另一个`try` / `catch`块再次抛出。与前面的例子不同,所有的异常都得到了正确的处理,事实上,不需要`noexcept(false)`来确保这段代码正确执行,因为对于抛出的每个异常,我们都有一个`try` / `catch`块。即使一个异常在析构函数中被抛出,它也得到正确的处理,这意味着析构函数安全地执行并保持`noexcept`兼容,即使第二个类在两个正在处理的异常存在的情况下执行。 # 轻松创建自己的异常类 在本食谱中,您将学习如何轻松创建自己的异常类型。这是需要学习的重要一课,因为虽然 C++ 异常很容易自己创建,但是应该遵循一些准则来确保安全地完成。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 执行以下步骤来尝试配方: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter02 ``` 2. 要编译源代码,请运行以下命令: ```cpp > mkdir build && cd build > cmake .. > make recipe05_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe05_example01 The answer is: 42 > ./recipe05_example02 The answer is: 42 > ./recipe05_example03 The answer is: 42 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 创建您自己的 C++ 异常允许您过滤出您得到的异常类型。例如,异常是来自您的代码还是 C++ 库?通过创建自己的 C++ 异常,您可以在运行时用自己的代码轻松回答这些问题。让我们看看下面的例子: ```cpp #include #include class the_answer : public std::exception { public: the_answer() = default; const char *what() const noexcept { return "The answer is: 42"; } }; int main(void) { try { throw the_answer{}; } catch (const std::exception &e) { std::cout << e.what() << '\n'; } } ``` 如前面的例子所示,我们通过继承`std::exception`来创建自己的 C++ 异常。这不是一个要求。从技术上讲,任何东西都可以是 C++ 异常,包括整数。然而,从`std::exception`开始,给了你一个标准的工作界面,包括覆盖`what()`函数,该函数描述了抛出的异常。 在前面的例子中,我们在`what()`函数中返回一个硬编码字符串。这是理想的异常类型(甚至比 C++ 库提供的异常更理想)。这是因为这种类型的异常是`nothrow copy-constructable`。具体来说,这意味着可以复制异常本身,而副本不会生成异常,例如由于`std::bad_alloc`。C++ 库提供的异常类型支持从`std::string()`开始构造,这可能会抛出`std::bad_alloc`。 前面的 C++ 异常的问题是,对于您希望提供的每种类型的消息,您都需要`1`异常类型。实现安全异常类型的另一种方法是使用以下内容: ```cpp #include #include class the_answer : public std::exception { const char *m_str; public: the_answer(const char *str): m_str{str} { } const char *what() const noexcept { return m_str; } }; int main(void) { try { throw the_answer("42"); } catch (const std::exception &e) { std::cout << "The answer is: " << e.what() << '\n'; } } ``` 在前面的例子中,我们存储了一个指向`const char*`的指针(即 C 风格的字符串)。c 风格的字符串在程序中作为常量全局存储。这种类型的异常满足前面所有相同的规则,并且在构建异常的过程中不会发生分配。还应该注意的是,由于字符串是全局存储的,这种类型的操作是安全的。 使用这种方法可以创建许多类型的异常,包括除了字符串之外的可以通过自定义 getters 访问的东西(也就是说,不必使用`what()`函数)。但是,如果前面的这些规则对您来说不是问题,创建自定义 C++ 异常的最简单方法是简单地将现有的 C++ 异常子类化,如`std::runtime_error()`,如下例所示: ```cpp #include #include #include class the_answer : public std::runtime_error { public: explicit the_answer(const char *str) : std::runtime_error{str} { } }; int main(void) { try { throw the_answer("42"); } catch (const the_answer &e) { std::cout << "The answer is: " << e.what() << '\n'; } catch (const std::exception &e) { std::cout << "unknown exception: " << e.what() << '\n'; } } ``` 执行此示例时,我们会得到以下结果: ![](img/a982b81c-4220-43df-82f3-73b32e54b2ae.png) 在前面的例子中,我们通过子类化`std::runtime_error()`,只用几行代码就创建了自己的 C++ 异常。然后我们可以使用不同的`catch`块来计算抛出了什么类型的异常。请记住,如果您使用`std::string`版本的`std::runtime_error()`,您可能会在异常本身的构建过程中被抛出`std::bad_alloc`。** ================================================ FILE: docs/adv-cpp-prog-cb/03.md ================================================ # 三、实现移动语义 在本章中,我们将学习一些高级的 C++ 移动语义。我们将首先讨论五大,这是一个习惯用法,只是鼓励程序员明确定义类的销毁和移动/复制语义。接下来,我们将学习如何定义移动构造函数和移动赋值运算符;移动语义的不同组合(包括仅移动和不可复制);不可移动类;以及如何实现这些类以及它们为什么重要。 本章还将讨论一些常见的陷阱,例如为什么`const &&`移动没有意义,以及如何克服 l 值和 r 值引用类型。本章中的方法很重要,因为一旦启用 C++ 11 或更高版本,就会启用移动语义,这将从根本上改变 C++ 在许多情况下处理类的方式。本章中的方法提供了用 C++ 编写高效代码的基础,这些代码的行为符合预期。 本章中的配方如下: * 使用编译器生成的特殊类成员函数和五大 * 让你的班级可移动 * 仅移动类型 * 实现`noexcept`移动构造器 * 学会警惕`const &&` * 引用限定成员函数 * 探索无法移动或复制的对象 # 技术要求 要编译和运行本章中的示例,您必须拥有运行 Ubuntu 18.04 的计算机的管理权限,并且具有功能性互联网连接。在运行这些示例之前,您必须安装以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 如果这安装在 Ubuntu 18.04 以外的任何操作系统上,则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。 # 使用编译器生成的特殊类成员函数和五大 当使用 C++ 11 或更高版本时,如果您没有在类定义中显式提供某些函数,编译器将自动为您的 C++ 类生成这些函数。在本食谱中,我们将探索这是如何工作的,编译器将为您创建哪些函数,以及这如何影响您的程序的性能和有效性。总的来说,这个方法的目标是说明每个类至少应该定义五大类,以确保您的类明确您希望如何管理资源。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来尝试此食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter03 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe01_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe01_example01 The answer is: 42 > ./recipe01_example02 The answer is: 42 > ./recipe01_example03 The answer is: 42 > ./recipe01_example04 The answer is: 42 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 在这个食谱中,我们将探索移动和复制之间的区别,以及这与五大函数的关系,五大函数是指所有类都应该明确定义的五个函数。首先,让我们看一个简单的例子,这个类在其构造函数中输出一个整数值: ```cpp class the_answer { int m_answer{42}; public: ~the_answer() { std::cout << "The answer is: " << m_answer << '\n'; } }; ``` 在上例中,当类被析构时,类将输出到`stdout`。该类还有一个在构造时初始化的整数成员变量。前面例子的问题是隐式复制和移动语义被抑制了,因为我们定义了类的析构函数。 五大函数是以下函数,如果至少定义了其中一个函数,每个类都应该定义这些函数(也就是说,如果定义了一个函数,就必须定义所有函数): ```cpp ~the_answer() = default; the_answer(the_answer &&) noexcept = default; the_answer &operator=(the_answer &&) noexcept = default; the_answer(const the_answer &) = default; the_answer &operator=(const the_answer &) = default; ``` 如图所示,五大类包括析构函数、移动构造函数、移动赋值运算符、复制构造函数和复制赋值运算符。这些类的作者不需要实现这些功能,而是应该——至少——*定义*功能,明确说明删除、复制和移动应该如何进行(如果有的话)。这确保了如果定义了其中一个函数,类的其余移动、复制和销毁语义都是正确的,如本例所示: ```cpp class the_answer { int m_answer{42}; public: the_answer() { std::cout << "The answer is: " << m_answer << '\n'; } public: virtual ~the_answer() = default; the_answer(the_answer &&) noexcept = default; the_answer &operator=(the_answer &&) noexcept = default; the_answer(const the_answer &) = default; the_answer &operator=(const the_answer &) = default; }; ``` 在前面的例子中,通过定义一个虚拟析构函数,这个类被标记为`virtual`(意味着这个类能够参与运行时多态)。不需要实现(通过将析构函数设置为`default`,但是定义本身是显式的,这告诉编译器我们希望类支持虚函数。这告诉该类的用户,指向该类的指针可用于删除从该类派生的任何类的实例。它还告诉用户继承将利用运行时多态性,而不是合成。这个类还声明复制和移动都是允许的。 让我们看另一个例子: ```cpp class the_answer { int m_answer{42}; public: the_answer() { std::cout << "The answer is: " << m_answer << '\n'; } public: ~the_answer() = default; the_answer(the_answer &&) noexcept = default; the_answer &operator=(the_answer &&) noexcept = default; the_answer(const the_answer &) = delete; the_answer &operator=(const the_answer &) = delete; }; ``` 在前面的示例中,拷贝被显式删除(这与定义移动构造函数而不定义拷贝语义相同)。这定义了一个只能移动的类,这意味着该类只能被移动;它不能被复制。标准库中这样的类的一个例子是`std::unique_ptr`。 下一个类实现相反的情况: ```cpp class the_answer { int m_answer{42}; public: the_answer() { std::cout << "The answer is: " << m_answer << '\n'; } public: ~the_answer() = default; the_answer(the_answer &&) noexcept = delete; the_answer &operator=(the_answer &&) noexcept = delete; the_answer(const the_answer &) = default; the_answer &operator=(const the_answer &) = default; }; ``` 在前面的例子中,我们已经明确定义了一个只复制类。 五大有很多不同的组合。这个方法的要点是表明,显式定义这五个函数可以确保类的作者明确了解类本身的意图。这与它应该如何操作以及用户应该如何使用类有关。显式确保类的作者不打算使用一种行为,而是获得另一种行为,因为编译器将如何基于编译器的实现以及 C++ 规范是如何定义的来隐式构造类。 # 让你的班级可移动 在 C++ 11 或更高版本中,可以复制或移动对象,这可以用来指示如何管理对象的资源。拷贝和移动的最大区别很简单:拷贝创建一个对象管理的资源的拷贝,而移动将资源从一个对象转移到另一个对象。 在这个食谱中,我们将解释如何使一个类可移动,包括如何正确添加移动构造函数和移动赋值操作符。我们还将解释可移动类的一些微妙细节,以及如何在代码中使用它们。这个方法很重要,因为在很多情况下,移动一个对象而不是复制一个对象可以提高性能并减少程序的内存消耗。然而,如果使用不当,可移动物体的使用可能会带来一些不稳定性。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来尝试此食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter03 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe02_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe02_example01 The answer is: 42 > ./recipe02_example02 The answer is: 42 The answer is: 42 The answer is: 42 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 在这个食谱中,我们将学习如何使一个类可移动。首先,让我们检查一个基本的类定义: ```cpp #include class the_answer { int m_answer{42}; public: the_answer() = default; public: ~the_answer() { std::cout << "The answer is: " << m_answer << '\n'; } }; int main(void) { the_answer is; return 0; } ``` 在前面的例子中,我们创建了一个简单的类,它有一个初始化的私有整数成员。然后,我们定义一个默认构造函数和一个析构函数,当类的一个实例被销毁时,它们输出到`stdout`。默认情况下,这个类是可移动的,但是移动操作模仿拷贝(换句话说,在这个简单的例子中,移动和拷贝没有区别)。 为了真正使这个类可移动,我们需要添加一个移动构造函数和一个移动赋值操作符,如下所示: ```cpp the_answer(the_answer &&other) noexcept; the_answer &operator=(the_answer &&other) noexcept; ``` 一旦我们添加了这两个函数,我们将能够使用以下内容将我们的类从一个实例移动到另一个实例: ```cpp instance2 = std::move(instance1); ``` 为了支持这一点,在前面的类中,我们不仅将添加移动构造函数和赋值操作符,还将实现一个默认构造函数,为我们的示例类提供一个有效的移动状态,如下所示: ```cpp #include class the_answer { int m_answer{}; public: the_answer() = default; explicit the_answer(int answer) : m_answer{answer} { } ``` 如图所示,该类现在有一个默认构造函数和一个接受整数参数的显式构造函数。默认构造函数初始化整数内存变量,该变量表示我们的移出或无效状态: ```cpp public: ~the_answer() { if (m_answer != 0) { std::cout << "The answer is: " << m_answer << '\n'; } } ``` 如上例所示,当类被破坏时,我们输出整数成员变量的值,但是在这种情况下,我们首先检查以确保整数变量有效: ```cpp the_answer(the_answer &&other) noexcept { *this = std::move(other); } the_answer &operator=(the_answer &&other) noexcept { if (&other == this) { return *this; } m_answer = std::exchange(other.m_answer, 0); return *this; } the_answer(const the_answer &) = default; the_answer &operator=(const the_answer &) = default; }; ``` 最后,我们实现了移动构造函数和赋值操作符。移动构造函数只是调用移动赋值操作符,以避免重复(因为它们执行相同的操作)。移动分配操作符首先检查以确保我们没有移动到自己身上。这是因为这样做会导致损坏,因为用户会期望类仍然包含有效的整数,但实际上,内部整数会无意中被设置为`0`。 然后我们交换整数值,并将原始值设置为`0`。这是因为,再一次,移动不是复制。移动会将值从一个实例转移到另一个实例。在这种情况下,被移动到的实例从`0`开始,并被赋予一个有效的整数,而被移动到的实例从一个有效的整数开始,并在移动后被设置为`0`,导致只有`1`实例包含一个有效的整数。 It should also be noted that we have to define the copy constructor and assignment operator. This is because, by default, if you provide a move constructor and assignment operator, C++ will automatically delete the copy constructor and assignment operator if they are not explicitly defined. 在本例中,我们将比较移动和复制,因此我们定义了复制构造函数和赋值操作符,以确保它们不会被隐式删除。一般来说,最好的做法是为您定义的每个类定义析构函数、移动构造函数和赋值操作符,以及复制构造函数和赋值操作符。这确保了您编写的每个类的复制/移动语义都是显式的和有意的: ```cpp int main(void) { { the_answer is; the_answer is_42{42}; is = is_42; } std::cout << '\n'; { the_answer is{23}; the_answer is_42{42}; is = std::move(is_42); } return 0; } ``` 当执行前面的代码时,我们会得到以下结果: ![](img/bc7cc97b-8542-42e5-9ada-0634f0017fbc.png) 在我们的主要功能中,我们运行两个不同的测试: * 第一个测试创建了我们类的两个实例,并将一个实例的内容复制到另一个实例中。 * 第二个测试创建了我们类的两个实例,然后将一个实例的内容移动到另一个实例。 当这个例子被执行时,我们看到第一个测试的输出被写入了两次。这是因为我们的类的第一个实例被赋予了我们的类的第二个实例的副本,该副本具有有效的整数值。第二个测试的输出只被写入一次,因为我们正在将一个实例的有效状态转移到另一个实例,导致在任何给定时刻只有一个实例具有有效状态。 这里有一些值得一提的显著例子: * 移动构造函数和赋值操作符永远不应该抛出异常。具体来说,移动操作将一个类型实例的有效状态转移到该类型的另一个实例。这个操作在任何时候都不会失败,因为没有创建或销毁任何状态。它只是被转移了。此外,在移动过程中,有时很难撤销部分移动操作。由于这些原因,这些功能应始终标记为`noexcept`(参考[https://github . com/isocpp/cppcoregories/blob/master/cppcoregories . MD # Rc-move-no except](https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rc-move-noexcept))。 * 移动构造函数和赋值操作符的函数签名中不包含`const`类型,因为被移动的实例不能是`const`,因为它的内部状态正在被转移,这隐含地假设正在发生写操作。更重要的是,如果您将移动构造函数或赋值操作符标记为`const`,则可能会出现副本。 * 除非您打算创建副本,否则应改用移动,尤其是对于大型对象。就像传递`const T&`作为函数参数来防止复制发生一样,当调用函数时,当资源被移动到另一个变量而不是被复制时,应该使用移动来代替复制。 * 编译器会在可能的情况下自动生成移动操作,而不是复制操作。例如,如果您在函数中创建一个对象,配置该对象,然后返回该对象,编译器将自动执行移动。 现在,您已经知道了如何使您的类可移动,在下一个食谱中,我们将学习什么是只移动类型,以及为什么您可能想要在您的应用中使用它们。 # 仅移动类型 在这个食谱中,我们将学习如何使一个类只移动。复制和移动之间区别的一个很好的例子是`std::unique_ptr`和`std::shared_ptr`之间的区别。 `std::unique_ptr`的要点是对动态分配的类型强制一个所有者,而`std::shared_ptr`则允许动态分配类型的多个所有者。两者都允许用户将指针类型的内容从一个实例化移动到另一个实例化,但是只有`std::shared_ptr`允许用户复制指针(因为复制指针会创建多个所有者)。 在这个食谱中,我们将使用这两个类来展示如何创建一个只移动的类,并展示为什么这种类型的类在 C++ 中被如此频繁地使用(因为大多数时候我们希望移动而不是复制)。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来尝试此食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter03 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe03_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe03_example01 The answer is: 42 > ./recipe03_example03 count: 2 The answer is: 42 The answer is: 42 count: 1 The answer is: 42 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 仅移动类是可以移动但不能复制的类。为了探索这种类型的类,让我们在下面的例子中包装`std::unique_ptr`,它本身是一个只移动的类: ```cpp class the_answer { std::unique_ptr m_answer; public: explicit the_answer(int answer) : m_answer{std::make_unique(answer)} { } ~the_answer() { if (m_answer) { std::cout << "The answer is: " << *m_answer << '\n'; } } public: the_answer(the_answer &&other) noexcept { *this = std::move(other); } the_answer &operator=(the_answer &&other) noexcept { m_answer = std::move(other.m_answer); return *this; } }; ``` 前面的类将`std::unique_ptr`存储为成员变量,并在构造时用整数值实例化内存变量。销毁时,该类检查以确保`std::unique_ptr`有效,如果有效,则将该值输出到`stdout`。 乍一看,我们可能想知道为什么我们必须检查有效性,因为`std::unique_ptr`总是被构造的。`std::unique_ptr`无效的原因是在移动过程中。因为我们正在创建一个只移动的类(而不是一个不可复制、不可移动的类),我们实现了移动构造函数和移动赋值操作符,这将移动`std::unique_ptr`。`std::unique_ptr`在移动时,会将其内部指针的内容从一个类转移到另一个类,导致该类因存储无效指针(即`nullptr`)而被移动。换句话说,即使这个类不能被空构造,如果它被移动,它仍然可以存储`nullptr`,如下例所示: ```cpp int main(void) { the_answer is_42{42}; the_answer is = std::move(is_42); return 0; } ``` 如前例所示,只有一个类输出到`stdout`,因为只有一个实例有效。像`std::unique_ptr`一样,一个只移动的类确保你在被创建的资源总数和实际发生的实例总数之间总是有 1:1 的关系。 需要注意的是,由于我们使用的是`std::unique_ptr`,所以不管我们喜不喜欢,我们的类都变成了只动类。例如,试图添加复制构造函数或复制赋值运算符来启用复制功能将导致编译错误: ```cpp the_answer(const the_answer &) = default; the_answer &operator=(const the_answer &) = default; ``` 换句话说,每个包含只移动类作为成员的类本身也变成了只移动类。虽然这看起来不可取,但你必须首先问自己:你真的需要一个类来复制吗?可能的答案是否定的。事实上,在大多数情况下,甚至在 C++ 11 之前,我们使用的大多数(如果不是全部)类都应该是只移动的。当一个类应该被移动时,它被复制的能力会导致资源浪费、损坏等等,这也是移动语义被添加到规范中的原因之一。移动语义允许我们定义我们希望如何处理我们分配的资源,并且它为我们提供了一种在编译时实施所需语义的方法。 您可能想知道如何将前面的示例转换为允许复制。以下示例利用共享指针来实现这一点: ```cpp #include #include class the_answer { std::shared_ptr m_answer; public: the_answer() = default; explicit the_answer(int answer) : m_answer{std::make_shared(answer)} { } ~the_answer() { if (m_answer) { std::cout << "The answer is: " << *m_answer << '\n'; } } auto use_count() { return m_answer.use_count(); } ``` 前面的类用`std::shared_ptr`代替`std::unique_ptr`。在引擎盖下,`std::shared_ptr`会记录副本的数量,只有当副本总数为`0`时,才会删除它存储的指针。事实上,您可以使用`use_count()`功能查询总份数。 接下来,我们定义移动构造函数、移动赋值运算符、复制构造函数和复制赋值运算符,如下所示: ```cpp public: the_answer(the_answer &&other) noexcept { *this = std::move(other); } the_answer &operator=(the_answer &&other) noexcept { m_answer = std::move(other.m_answer); return *this; } the_answer(const the_answer &other) { *this = other; } the_answer &operator=(const the_answer &other) { m_answer = other.m_answer; return *this; } }; ``` 这些定义也可以使用`=`默认语法编写,因为这些实现是相同的。最后,我们使用以下内容测试这个类: ```cpp int main(void) { { the_answer is_42{42}; the_answer is = is_42; std::cout << "count: " << is.use_count() << '\n'; } std::cout << '\n'; { the_answer is_42{42}; the_answer is = std::move(is_42); std::cout << "count: " << is.use_count() << '\n'; } return 0; } ``` 如果我们执行前面的代码,我们会得到以下结果: ![](img/80128ca4-0b35-4b29-b649-c871a64b025f.png) 在前面的测试中,我们首先创建一个类的副本,并输出副本总数,以查看实际上创建了两个副本。第二个测试执行`std::move()`而不是拷贝,这导致只按照预期创建了一个拷贝。 # 实现 noexcept 移动构造函数 在本食谱中,我们将学习如何确保移动构造函数和移动赋值运算符永远不会抛出异常。C++ 规范并不阻止移动构造函数抛出(因为已经确定这样的要求很难执行,因为即使在标准库中也存在太多合法的例子)。然而,在大多数情况下,确保不抛出异常应该是可能的。具体来说,移动通常不会创建资源,而是转移资源,因此,强异常保证应该是可能的。创建资源的移动的一个很好的例子是`std::list`,它必须提供一个有效的`end()`迭代器,即使是在移动中。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来尝试此食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter03 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe04_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe04_example01 failed to move The answer is: 42 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 如前所述,移动不应该抛出异常,以确保强异常保证(也就是说,移动对象的行为不会损坏对象),在大多数情况下,这是可能的,因为移动(不像复制)不会创建资源,而是转移资源。确保移动构造函数和移动赋值操作符不抛出的最佳方法是仅使用`std::move()`转移成员变量,如下例所示: ```cpp m_answer = std::move(other.m_answer); ``` 假设您正在移动的成员变量没有抛出,那么您的类也不会抛出。使用这个简单的技术将确保您的移动构造函数和操作符永远不会抛出。但是如果这个操作不能用呢?让我们用下面的例子来探讨这个问题: ```cpp #include #include class the_answer { std::vector m_answer; public: the_answer() = default; explicit the_answer(int answer) : m_answer{{answer}} { } ~the_answer() { if (!m_answer.empty()) { std::cout << "The answer is: " << m_answer.at(0) << '\n'; } } ``` 在前面的例子中,我们创建了一个以向量为成员变量的类。默认情况下,向量可以初始化为空,也可以用单个元素初始化。销毁时,如果向量有值,我们将值输出到`stdout`。我们实现`move`构造函数和运算符如下: ```cpp public: the_answer(the_answer &&other) noexcept { *this = std::move(other); } the_answer &operator=(the_answer &&other) noexcept { if (&other == this) { return *this; } try { m_answer.emplace(m_answer.begin(), other.m_answer.at(0)); other.m_answer.erase(other.m_answer.begin()); } catch(...) { std::cout << "failed to move\n"; } return *this; } }; ``` 如图所示,move 操作符将单个元素从一个实例转移到另一个实例(这不是实现移动的最佳方式,但是这个实现可以演示这一点,而不会过于复杂)。如果向量为空,此操作将抛出,如下例所示: ```cpp int main(void) { { the_answer is_42{}; the_answer is_what{}; is_what = std::move(is_42); } std::cout << '\n'; { the_answer is_42{42}; the_answer is_what{}; is_what = std::move(is_42); } return 0; } ``` 最后,我们尝试在两个不同的测试中移动这个类的一个实例。在第一个测试中,两个实例都是默认构造的,这导致空类,而第二个测试用单个元素构造向量,这导致有效的移动。在这种情况下,我们能够防止移动被抛出,但是应该注意的是,结果类实际上并没有执行移动,导致两个对象都不包含所需的状态。这就是为什么移动构造函数永远不应该抛出。即使我们没有捕捉到异常,在抛出发生后断言程序的状态也是极其困难的。搬家发生了吗?每个实例处于什么状态?在大多数情况下,这种类型的错误会导致在程序进入损坏状态时调用`std::terminate()`。 副本是不同的,因为原始类保持不变。复制是无效的,程序员可以很好地处理这种情况,因为被复制的实例的原始状态不受影响(因此我们将其标记为`const`)。 但是,由于被移动的实例是可写的,两个实例都处于损坏状态,并且没有好的方法知道如何处理向前移动的程序,因为我们不知道原始实例是否处于可以正确处理的状态。 # 学会警惕 const&& 在本食谱中,我们将学习为什么移动构造函数或运算符永远不应该标记为`const`(为什么复制构造函数/运算符总是标记为`const`)。这一点很重要,因为它触及到了移动和复制之间区别的核心。C++ 中的 Move 语义是它最强大的特性之一,理解它为什么如此重要以及它实际在做什么对于编写好的 C++ 代码至关重要。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来尝试此食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter03 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe05_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe05_example01 copy ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 在本食谱中,我们将了解为什么`const&&`构造函数或运算符没有意义,并将导致意想不到的行为。移动会转移资源,这就是为什么它被标记为非`const`。这是因为传输假设两个实例都被写入(一个实例接收资源,而另一个实例取走资源)。副本创建资源,这就是为什么它们不总是被标记为`noexcept`(创建资源绝对可以抛出)并且它们被标记为`const`(因为原始实例是被复制的,而不是被修改的)。一个`const&&`构造函数声称是一个不转移的移动,它必须是一个副本(如果你没有写入原始实例,你没有移动——你在复制),如本例所示: ```cpp #include class copy_or_move { public: copy_or_move() = default; public: copy_or_move(copy_or_move &&other) noexcept { *this = std::move(other); } copy_or_move &operator=(copy_or_move &&other) noexcept { std::cout << "move\n"; return *this; } copy_or_move(const copy_or_move &other) { *this = other; } copy_or_move &operator=(const copy_or_move &other) { std::cout << "copy\n"; return *this; } }; int main(void) { const copy_or_move test1; copy_or_move test2; test2 = std::move(test1); return 0; } ``` 在前面的示例中,我们创建了一个实现默认移动和复制构造函数/运算符的类。唯一不同的是,我们将输出添加到`stdout`来告诉我们是正在执行拷贝还是正在执行移动。 然后,我们创建了两个类实例,实例被从标记为`const`的位置移走。然后我们执行移动,输出的是一个副本。这是因为即使我们要求移动,编译器也使用了副本。我们可以实现一个`const &&`移动构造函数/操作符,但是没有办法将移动写成移动,因为我们将被移动的对象标记为`const`,所以我们不能获取它的资源。事实上,这样的移动将被实现为一个副本,与编译器自动为我们做的一样。 在下一个配方中,我们将学习如何向成员函数添加限定符。 # 引用限定成员函数 在这个食谱中,我们将了解什么是引用限定成员函数。虽然 C++ 语言的这一方面较少被使用和理解,但它很重要,因为它为程序员提供了处理资源如何操作的能力,这取决于调用函数时类是处于 l 值还是 r 值状态。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来尝试此食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter03 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe06_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe06_example01 the answer is: 42 the answer is not: 0 the answer is not: 0 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 在本例中,我们将了解什么是引用限定成员函数。为了解释什么是引用限定成员函数,让我们看下面的例子: ```cpp #include class the_answer { public: ~the_answer() = default; void foo() & { std::cout << "the answer is: 42\n"; } void foo() && { std::cout << "the answer is not: 0\n"; } public: the_answer(the_answer &&other) noexcept = default; the_answer &operator=(the_answer &&other) noexcept = default; the_answer(const the_answer &other) = default; the_answer &operator=(const the_answer &other) = default; }; ``` 在这个例子中,我们实现了一个`foo()`函数,但是我们有两个不同的版本。第一个版本结尾有`&`,第二个版本结尾有`&&`。执行哪个`foo()`函数取决于实例是 l 值还是 r 值,如下例所示: ```cpp int main(void) { the_answer is; is.foo(); std::move(is).foo(); the_answer{}.foo(); } ``` 执行时会产生以下结果: ![](img/19571c4b-ebb1-4680-a183-82571ec2416c.png) 如前例所示,`foo()`的第一次执行是一个 l 值,因为执行的是`foo()`的 l 值版本(也就是最后有`&`的函数)。`foo()`的最后两次执行是 r 值,因为执行的是`foo()`的 r 值版本。 引用限定的成员函数可用于确保该函数仅在正确的上下文中调用。使用这些类型的函数的另一个原因是确保只有当 l 值或 r 值引用存在时才调用该函数。 例如,您可能不希望将`foo()`作为 r 值调用,因为这种类型的调用不能确保类的实例在调用本身之外有一个生存期,如前面的示例所示。 在下一个食谱中,我们将学习如何制作一个既不能移动也不能复制的类,并解释为什么你可能会做这样的事情。 # 探索无法移动或复制的对象 在本食谱中,我们将学习如何创建一个我们不能移动或复制的对象,以及为什么您可能想要创建这样一个类。复制类需要复制类的内容,这在某些情况下是不可能的(例如,复制内存池并不简单)。移动一个类假设该类被允许以一种潜在的无效状态存在(例如,`std::unique_ptr`在移动时采用一个`nullptr`值,该值是无效的)。这种情况也可能是不可取的(你现在必须检查有效性)。我们无法复制的不可移动类可以克服这些类型的问题。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来尝试此食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter03 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe07_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe07_example01 The answer is: 42 Segmentation fault (core dumped) ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 仅移动类防止类被复制,这在某些情况下可以提高性能。仅移动类还确保了创建的资源与分配的资源之间的 1:1 关系,因为副本不存在。但是,移动类可能会导致类无效,如本例所示: ```cpp #include class the_answer { std::unique_ptr m_answer; public: explicit the_answer(int answer) : m_answer{std::make_unique(answer)} { } ~the_answer() { std::cout << "The answer is: " << *m_answer << '\n'; } public: the_answer(the_answer &&other) noexcept = default; the_answer &operator=(the_answer &&other) noexcept = default; }; int main(void) { the_answer is_42{42}; the_answer is_what{42}; is_what = std::move(is_42); return 0; } ``` 如果我们运行前面的代码,我们会得到以下结果: ![](img/f6a0a4c9-5084-4fae-8a30-69fb5fff3ce5.png) 在前面的例子中,我们创建了一个可以移动的类,它存储`std::unique_ptr`。在类的析构函数中,我们取消引用该类并输出它的值。我们不检查`std::unique_ptr`的有效性,因为我们编写了一个强制有效`std::unique_ptr`的构造函数,却忘记了一个动作可以撤销这个显式的有效性。结果是,当执行一个移动时,我们得到一个分割错误。 为了克服这一点,我们需要提醒一下,我们做出了如下假设: ```cpp class the_answer { std::unique_ptr m_answer; public: explicit the_answer(int answer) : m_answer{std::make_unique(answer)} { } ~the_answer() { std::cout << "The answer is: " << *m_answer << '\n'; } public: the_answer(the_answer &&other) noexcept = delete; the_answer &operator=(the_answer &&other) noexcept = delete; the_answer(const the_answer &other) = delete; the_answer &operator=(const the_answer &other) = delete; }; ``` 前面的类显式删除了复制和移动操作,这是我们想要的意图。现在,如果我们不小心移动了这个类,我们会得到以下结果: ```cpp /home/user/book/chapter03/recipe07.cpp: In function ‘int main()’: /home/user/book/chapter03/recipe07.cpp:106:30: error: use of deleted function ‘the_answer& the_answer::operator=(the_answer&&)’ is_what = std::move(is_42); ^ /home/user/book/chapter03/recipe07.cpp:95:17: note: declared here the_answer &operator=(the_answer &&other) noexcept = delete; ^~~~~~~~ ``` 这个错误告诉我们,假设类是有效的,因此不支持移动。我们要么需要适当的支持移动(这意味着我们必须保持对无效`std::unique_ptr`的支持),要么我们需要移除`move`操作。如图所示,一个不能被移动或复制的类可以确保我们的代码按预期工作,为编译器提供了一种机制,当我们用我们的类做一些我们不打算做的事情时,它会警告我们。 ================================================ FILE: docs/adv-cpp-prog-cb/04.md ================================================ # 四、将模板用于泛型编程 在本章中,我们将学习高级模板编程技术。这些技术包括根据提供的类型更改模板类实现的能力,如何处理不同类型的参数,包括如何正确转发它们,如何在运行时和编译时优化代码,以及如何使用 C++ 17 中添加的一些新功能。这一点很重要,因为它可以更好地理解模板编程是如何工作的,以及如何确保模板按照您期望的方式运行。 很多时候,我们编写模板代码时假设它是以一种方式执行的,而实际上,它是以另一种方式执行的,要么生成不可靠的代码,要么生成有意外性能损失的代码,要么两者都有。本章将解释如何避免这些问题,并为编写合适的通用程序提供基础。 本章中的配方如下: * 实施 SFINAE * 学习完美转发 * 使用`if constexpr` * 使用元组处理参数包 * 使用特征来改变模板实现的行为 * 学习如何实施`template` * 使用显式模板声明 # 技术要求 要编译和运行本章中的示例,您必须拥有运行 Ubuntu 18.04 的计算机的管理权限,并且具有功能性互联网连接。在运行这些示例之前,请安装以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 如果这安装在 Ubuntu 18.04 以外的任何操作系统上,则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。 # 实施 SFINAE 在本食谱中,我们将学习如何使用**替代失败不是错误** ( **SFINAE** )。这个方法很重要,因为通常我们创建模板时并没有确保传递给模板的类型是我们所期望的。这可能导致意想不到的行为、次优的性能,甚至是错误的、不可靠的代码。 SFINAE 允许我们明确在我们的模板中期望什么类型。它还为我们提供了一种方法,可以根据提供的类型来改变模板的行为。对于一些人来说,SFINAE 的问题是这个概念很难理解。我们在这个食谱中的目标是揭开 SFINAE 的神秘面纱,并展示如何在自己的代码中使用它。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来尝试此食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter04 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe01_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe01_example01 The answer is: 23 The answer is: 42 > ./recipe01_example02 The answer is: 42 > ./recipe01_example03 The answer is: 42 > ./recipe01_example04 The answer is: 42 > ./recipe01_example05 The answer is: 42 The answer is: 42 The answer is: 42.12345678 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 在这个食谱中,你将学习如何在你自己的代码中加入 SFINAE。首先,我们必须先了解什么是 SFINAE,标准库如何使用它来实现`type`特性。不知道`type`特性是如何实现的,就很难理解如何使用它们。 首先,用 SFINAE 最重要的是要理解它的名字是怎么说的,那就是一个*的换人失败并不是一个错误*。这意味着当模板类型被替换时,如果发生故障,编译器将*而不是*产生错误。例如,我们可以编写以下内容: ```cpp #include struct the_answer { using type = unsigned; }; template void foo(typename T::type t) { std::cout << "The answer is not: " << t << '\n'; } template void foo(T t) { std::cout << "The answer is: " << t << '\n'; } int main(void) { foo(23); foo(42); return 0; } ``` 这里描述了其中每一个的输出: ```cpp The answer is: 23 The answer is: 42 ``` 在这个例子中,我们已经创建了两个版本的`foo()`函数。第一个版本采用了一个具有 T2 别名的类型,我们用它来创建函数的参数。第二个版本只是取`T`类型本身。然后我们使用两个版本的`foo()`函数,一个带有整数,另一个带有定义`type`别名的结构。 前面例子的要点是,当我们调用`foo()`函数的`foo()`版本时,当编译器试图将`int`类型与采用带有`type`别名的类型的`foo()`函数的版本相匹配时,它不会产生错误。这就是 SFINAE。它只是说,当编译器尝试采用给定的类型并将其与模板匹配时,如果出现故障,编译器将不会生成错误。唯一会发生错误的情况是编译器找不到合适的替代。比如我们评论出第二版`foo()`会怎么样?让我们看看: ![](img/84d28ad2-c0bd-49a0-879d-ad42f5add912.png) 从前面的错误输出可以看出,编译器甚至说该错误是替换错误。根据提供的类型,我们提供的模板不是有效的候选模板。 这个例子的另一个重要收获是,编译器能够根据提供的类型在我们的`foo()`函数的两个不同版本之间进行选择。我们可以利用这一点。具体来说,这使我们能够根据所提供的类型做不同的事情。我们所需要的是一种方法来编写我们的`foo()`函数,这样我们就可以根据我们提供的类型来启用/禁用不同版本的模板。 这就是`std::enable_if`发挥作用的地方。`std::enable_if`将 SFINAE 的思想带到下一步,允许我们定义一个类型,如果它的参数为真。否则,它将生成替换错误,故意迫使编译器选择模板的不同版本。`std::enable_if`定义如下: ```cpp template struct enable_if {}; template struct enable_if { typedef T type; }; ``` 这首先定义了一个采用`bool B`的结构和一个默认为`void`的`T`类型。当`bool`为真时,它定义了这种`struct`类型的特殊化。具体来说,当`bool`值为`true`时,返回所提供的类型,如前所述,默认为`void`。要了解这是如何使用的,让我们看一个例子: ```cpp #include #include template constexpr auto is_int() { return false; } template<> constexpr auto is_int() { return true; } template< typename T, std::enable_if_t(), int> = 0 > void the_answer(T is) { std::cout << "The answer is: " << is << '\n'; } int main(void) { the_answer(42); return 0; } ``` 输出如下: ![](img/395bc1d6-02a2-4609-be80-8855f53d6acc.png) 在这个例子中,我们创建了一个名为`is_int()`的函数,它总是返回`false`。然后我们为返回`true`的`int`创建这个函数的模板专门化。接下来,我们创建一个接受任何类型的函数,但是我们将`std::enable_if_t`(添加的`_t`部分是 C++ 17 中为`::type`添加的简写)添加到使用我们的`is_int()`函数的模板定义中。如果提供的`T`类型是`int`,我们的`is_int()`功能将返回`true`。 `std::enable_if`默认不做任何事情。但是,如果是`true`,它会返回一个`type`别名,在前面的示例中,它是我们作为`std::enable_if`的第二个参数传递的`int`类型。这是说如果`std::enable_if`是`true`,它会返回一个`int`类型。然后我们将这个`int`类型设置为`0`,这是一个有效的做法。这不会导致失败;我们的模板函数成为一个有效的替代,因此被使用。综上所述,如果`T`是`int`类型,`std::enable_if`变成了`int`类型本身,然后我们设置为`0`,编译没有问题。如果我们的`T`型不是`int`,那么`std::enable_if`就变成了虚无。试图将 nothing 设置为`0`会导致编译错误,但由于这是 SFINAE,编译器错误只会变成替换错误。 让我们看看错误案例。如果我们将`42`设置为`42.0`,这是一个`double`,而不是`int`,我们会得到以下结果: ![](img/3eadc479-fa00-4c25-9a68-b82cb13ee914.png) 从前面的错误可以看出,编译器是在说`enable_if`中没有名为`type`的类型。如果看`std::enable_if`的定义,这是意料之中的,因为`std::enable_if`如果是假的就什么都不做。它只创建一个名为`type`的类型,如果它是真的。 为了更好地理解这是如何工作的,让我们看另一个例子: ```cpp #include #include template< typename T, std::enable_if_t>* = nullptr > void the_answer(T is) { std::cout << "The answer is: " << is << '\n'; } int main(void) { the_answer(42); return 0; } ``` 输出如下: ![](img/cb55d047-89b4-4e40-815a-273456762831.png) 在前面的例子中,我们使用了`std::is_integral_v`,它和我们的`is_int()`函数做同样的事情,不同的是它是由标准库提供的,可以处理 CV 类型。事实上,标准库有这些函数的不同版本的大量列表,包括不同的类型、继承属性、CV 属性等等。如果您需要检查任何类型的`type`属性,标准库有一个您可以使用的`std:is_xxx`功能。 前面的例子与我们前面的例子几乎相同,不同之处在于我们在`std::enable_if`方法中不返回`int`。相反,我们使用`* = nullptr`。这是因为`std::enable_if`默认返回`void`。`*`字符将这个空位变成一个空位指针,然后我们将其设置为`nullptr`。 在下一个示例中,我们展示了这方面的另一个变化: ```cpp #include #include template std::enable_if_t> the_answer(T is) { std::cout << "The answer is: " << is << '\n'; } int main(void) { the_answer(42); return 0; } ``` 输出如下: ![](img/5ca6a189-e687-45a2-a8cd-422d3e2f274e.png) 在这个例子中,我们函数的`void`是由`std::enable_if`创建的。如果`T`不是整数,则不返回`void`,我们会看到这个错误(而不是代码编译并允许我们首先执行它): ![](img/d7ec86d0-edec-409d-8dd5-76b3abfa3978.png) 总之,`std::enable_if`将创建一个名为`type`的类型,它基于您提供的类型。默认情况下,这里是`void`,但是你可以输入任何你想要的类型。此功能不仅可用于为我们的模板强制一种类型,还可用于根据我们提供的类型定义不同的函数,如本例所示: ```cpp #include #include #include template< typename T, std::enable_if_t>* = nullptr > void the_answer(T is) { std::cout << "The answer is: " << is << '\n'; } template< typename T, std::enable_if_t>* = nullptr > void the_answer(T is) { std::cout << std::setprecision(10); std::cout << "The answer is: " << is << '\n'; } int main(void) { the_answer(42); the_answer(42U); the_answer(42.12345678); return 0; } ``` 前面代码的输出如下: ![](img/1c12f216-9fc9-4b34-9868-9ccf45ae4fb7.png) 就像我们在这个食谱中的第一个例子一样,我们已经创建了同一个函数的两个不同版本。SFINAE 允许编译器根据提供的类型选择最合适的版本。 # 学习完美转发 在这个食谱中,我们将学习如何使用完美转发。这个方法很重要,因为在编写模板时,我们通常会将模板参数传递给其他函数。如果我们不使用完美转发,我们可能会无意中将 r 值引用转换为 l 值引用,导致发生潜在的复制而不是移动,在某些情况下,这可能是次优的。完美转发还为编译器提供了一些提示,可以利用这些提示来改进函数的内联和展开。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来尝试此食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter04 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe02_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe02_example01 l-value l-value > ./recipe02_example02 l-value r-value > ./recipe02_example03 l-value: 42 r-value: 42 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 在这个食谱中,我们将学习如何使用完美的转发,以确保当我们在模板中传递参数时(也就是说,转发我们的参数),我们这样做的方式不会抹去 r 值。为了更好地理解这个问题,让我们看看下面的例子: ```cpp #include struct the_answer { }; void foo2(const the_answer &is) { std::cout << "l-value\n"; } void foo2(the_answer &&is) { std::cout << "r-value\n"; } template void foo1(T &&t) { foo2(t); } int main(void) { the_answer is; foo1(is); foo1(the_answer()); return 0; } ``` 输出如下: ![](img/eb4f4cb0-e924-4b6e-af1a-617c8e3183b0.png) 在前面的例子中,我们有两个不同版本的`foo()`函数:一个采用 l 值引用,一个采用 r 值引用。然后我们从一个模板函数中调用`foo()`。该模板函数采用转发引用(也称为通用引用),它是与`auto`或模板函数配对的 r 值引用。最后,从我们的主函数,我们调用我们的模板,看看调用了哪个`foo()`函数。我们第一次调用模板时,会传入一个 l 值。因为我们被赋予了一个 l 值,通用引用变成了 l 值,我们的`foo()`函数的 l 值版本被调用。问题是,我们第二次调用模板函数时,我们给了它一个 r 值,但它调用了我们的`foo()`函数的 l 值版本,尽管它被赋予了 r 值。 这里常见的错误是,即使模板函数采用通用引用,并且我们有一个版本的`foo()`函数也采用 r 值,我们假设这个`foo()`函数将被调用。斯科特·迈耶斯在他的许多关于普遍参考的讲座中很好地解释了这一点。问题是,当你使用一个通用参考时,它变成了一个 l 值。传递`names`参数的行为,这意味着它必须是一个 l 值。它强制编译器转换为 l 值,因为它看到您在使用它,即使您所做的只是传递参数。应该注意的是,我们的例子并不使用优化进行编译,因为如果编译器可以安全地确定变量没有被使用,它可以自由地优化 l 值。 为了防止这个问题,我们需要告诉编译器我们希望转发该参数。通常,我们会用`std::move()`来表示这个。问题是,如果最初给我们一个 l 值,我们就不能使用`std::move()`,因为那样会将 l 值转换成 r 值。这就是标准库有`std::forward()`的原因,它是使用以下内容实现的: ```cpp static_cast(t) ``` `std::forward()`所做的只是将参数转换回其原始参考类型。这告诉编译器,如果该参数最初是 r 值,则将其显式视为 r 值,如下例所示: ```cpp #include struct the_answer { }; void foo2(const the_answer &is) { std::cout << "l-value\n"; } void foo2(the_answer &&is) { std::cout << "r-value\n"; } template void foo1(T &&t) { foo2(std::forward(t)); } int main(void) { the_answer is; foo1(is); foo1(the_answer()); return 0; } ``` 输出如下: ![](img/c64d9b68-b5d8-4ce2-ba02-17195ee8906d.png) 前面的例子与第一个例子相同,唯一的区别是我们使用`std::forward()`在模板函数中传递参数。这一次,当我们用 r 值调用我们的模板函数时,它调用我们的`foo()`函数的 r 值版本。这叫**完美转发**。它确保我们在传递参数时保持 CV 属性和 l-/r 值属性。需要注意的是,完美转发只有在使用模板函数或`auto`时才有效。这意味着完美转发通常只在编写包装器时有用。标准库包装器的一个很好的例子是`std::make_unique()`。 像`std::make_unique()`这样的包装器的一个问题是,您可能不知道需要传递多少参数。也就是说,您可能最终需要包装器中的可变模板参数。完美转发通过以下方式支持这一点: ```cpp #include struct the_answer { }; void foo2(const the_answer &is, int i) { std::cout << "l-value: " << i << '\n'; } void foo2(the_answer &&is, int i) { std::cout << "r-value: " << i << '\n'; } template void foo1(Args &&...args) { foo2(std::forward(args)...); } int main(void) { the_answer is; foo1(is, 42); foo1(the_answer(), 42); return 0; } ``` 输出如下: ![](img/6a2956d6-ac8c-4113-b411-eb131555a556.png) 前面的例子之所以有效,是因为传递给我们的`foo()`函数的变量模板参数被逗号分隔的完美转发列表所取代。 # 使用 if constexpr 在这个食谱中,我们将学习如何在 C++ 17 中使用一个名为`constexpr if`的新功能。这个食谱很重要,因为它将教你如何创建在运行时评估的`if`语句。具体来说,这意味着分支逻辑是在编译时而不是运行时选取的。这允许您在编译时改变函数的行为,而不牺牲性能,这在过去只能用宏来完成,这在模板编程中是没有用的,正如我们将要展示的。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来尝试此食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter04 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe03_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe03_example01 The answer is: 42 > ./recipe03_example02 The answer is: 42 The answer is: 42.12345678 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 有时,我们希望改变程序的行为,但是我们正在创建的代码总是不变的,这意味着编译器能够确定分支本身的值,如下例所示: ```cpp if (!NDEBUG) {} ``` 这是很多代码中常用的`if`语句,包括标准库。如果启用调试,该代码的评估结果为`true`。我们通过在代码中添加调试语句来使用它,调试语句可以关闭。编译器足够聪明,可以看到`NDEBUG`是`true`还是`false`,并且要么添加代码,要么完全删除代码。换句话说,编译器可以进行简单的优化,减少代码的大小,并删除不需要的分支,因为它知道这个`if`语句的值在运行时永远不会改变。问题是,这个技巧依赖于编译器很聪明的事实。逻辑的移除是隐式可信的,这通常会导致对编译器正在做什么的假设。C++ 17 增加了一个`constexpr if`语句,允许我们改为显式。它允许我们告诉编译器:我提供的语句应该在编译时计算,而不是在运行时计算。真正强大的是,当这个假设不成立时,我们会得到编译时错误,这意味着我们隐式信任编译器执行的优化,我们现在可以在编译时验证,如果假设为假,我们会被告知可以修复问题,如下例所示: ```cpp #include constexpr auto answer = 42; int main(void) { if constexpr (answer == 42) { std::cout << "The answer is: " << answer << '\n'; } else { std::cout << "The answer is not: " << answer << '\n'; } return 0; } ``` 输出如下: ![](img/cf8bb6b4-07e7-4b3e-a97e-3a558cfc3533.png) 在前面的例子中,我们创建了`constexpr`并在编译时而不是运行时对其求值。如果我们将`constexpr`更改为实际变量,`constexpr if`将导致以下错误: ![](img/9474112b-7528-4649-b754-1e6702247c6c.png) 然后,我们可以在模板函数中使用它,根据给定的类型更改模板函数的行为,如下例所示: ```cpp #include #include template constexpr void foo(T &&t) { if constexpr (std::is_floating_point_v) { std::cout << std::setprecision(10); } std::cout << "The answer is: " << std::forward(t) << '\n'; } int main(void) { foo(42); foo(42.12345678); return 0; } ``` 在前面的例子中,我们使用`std::is_floating_point_v`类型特征来确定我们得到的类型是否是浮点型的。如果类型不是浮点,这将返回`constexpr false`,编译器可以优化出来。由于我们使用的是`constexpr if`,我们可以确保我们的`if`语句实际上是`constexpr`而不是运行时条件。 # 使用元组处理参数包 在本食谱中,我们将学习如何使用`std::tuple`处理可变参数列表。这一点很重要,因为变量参数列表用于包装函数,包装函数不知道传递给它的参数,而是将这些参数转发给传递给它的函数。但是,在一些用例中,您会关心传递的参数,并且您必须有一种处理这些参数的方法。这个食谱将展示如何做到这一点,包括如何处理任何数量的论点。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来尝试此食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter04 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe04_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe04_example01 > ./recipe04_example02 the answer is: 42 > ./recipe04_example03 The answer is: 42 > ./recipe04_example04 2 2 > ./recipe04_example05 The answer is: 42 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 变量模板为程序员提供了定义模板函数的能力,而不需要定义所有的参数。这些在包装函数中大量使用,因为它们防止包装器必须知道函数的参数,如本例所示: ```cpp #include template void foo(Args &&...args) { } int main(void) { foo("The answer is: ", 42); return 0; } ``` 如前面的例子所示,我们已经创建了一个`foo`函数,它可以接受任意数量的参数。在本例中,我们使用了通用引用符号`Args &&...args`,这确保了 CV 限定符和 l-/r-value 被保留,这意味着我们可以使用`std::forward()`将变量参数列表传递给任何其他函数,而性能损失尽可能小。`std::make_unique()`等功能大量使用变量参数。 但是,有时您可能想要访问提供的列表中的某个参数。为此,我们可以使用`std::tuple`。这是一个数据结构,接受可变数量的参数,并提供`std::get()`函数从`std::tuple`获取任何数据,如本例所示: ```cpp #include #include int main(void) { std::tuple t("the answer is: ", 42); std::cout << std::get<0>(t) << std::get<1>(t) << '\n'; return 0; } ``` 输出如下: ![](img/e3147713-d2d1-4d27-b867-d95407e67851.png) 在上例中,我们创建了`std::tuple`,然后使用`std:get()`功能将`std::tuple`的内容输出到`stdout`。如果您试图访问超出范围的数据,编译器会在编译时知道,并给您一个类似如下的错误: ![](img/f0d35dc0-05d0-44ae-9202-1dbc5da6503c.png) 使用`std::tuple`,我们可以从变量参数列表中访问数据,如下所示: ```cpp #include #include template void foo(Args &&...args) { std::tuple t(std::forward(args)...); std::cout << std::get<0>(t) << std::get<1>(t) << '\n'; } int main(void) { foo("The answer is: ", 42); return 0; } ``` 输出如下: ![](img/fac54cfd-01a0-4fc8-a4e1-80bb81c1fd5f.png) 在前面的例子中,我们创建了一个带有可变参数列表的函数。然后我们使用`std::forward()`将这个列表传递给`std::tuple`以保持 l/r 值。最后,我们使用`std::tuple`来访问参数。如果我们不使用`std::forward()`,我们最终会得到传递给`std::tuple`的数据的 l 值版本。 前面例子的明显问题是我们已经将`0`和`1`索引硬编码到了`std::tuple`中。变量参数不是运行时的动态参数数组。相反,它们是一种表达方式*我不在乎我被赋予的参数*,这就是为什么它们通常被包装器使用。包装器包装的是关心参数的东西。在`std::make_unique()`的情况下,功能是创建`std::unique_ptr`。为此,`std::make_unique()`将为您分配`std::unique_ptr`,使用变量参数列表初始化新分配的类型,然后为您提供指向此类型的指针到`std::unique_ptr`,如本例所示: ```cpp template< typename T, typename... Args > void make_unique(Args &&...args) { return unique_ptr(new T(std::forward(args)...)); } ``` 包装器不关心传递的参数。`T`的构造函数有。如果你试图访问变量参数,你是在说*我确实关心参数*,在这种情况下,如果你关心,你必须对正在传递的参数的布局有所了解。 但是,有一些技巧可以让您处理未知数量的参数。尝试这样做的最大问题是,处理变量参数的库工具最好在运行时使用,这在大多数情况下没有帮助,如下例所示: ```cpp #include #include template void foo(Args &&...args) { std::cout << sizeof...(Args) << '\n'; std::cout << std::tuple_size_v> << '\n'; } int main(void) { foo("The answer is: ", 42); return 0; } ``` 输出如下: ![](img/3e437b68-faa5-4a2b-a2e5-1353d8935542.png) 在前面的示例中,我们试图获取变量参数列表中参数数量的总大小。我们可以使用变量版本的`sizeof()`函数或者使用`std::tuple_size`特性来实现。问题是这在编译时没有帮助我们,因为我们不能使用这个大小信息循环参数(因为编译时逻辑没有`for`循环)。 为了克服这一点,我们可以使用一种叫做编译时递归的技巧。这个技巧使用模板来创建一个递归模板函数,它将循环遍历变量参数列表中的所有参数。看看这个例子: ```cpp #include #include template< std::size_t I = 0, typename ... Args, typename FUNCTION > constexpr void for_each(const std::tuple &t, FUNCTION &&func) { if constexpr (I < sizeof...(Args)) { func(std::get(t)); for_each(t, std::forward(func)); } } ``` 我们从一个执行所有魔法的模板函数开始。这个第一个模板参数是`I`,它是一个从`0`开始的整数。下一个是变量模板参数,最后一个是函数类型。我们的模板函数采用`std::tuple`,我们希望迭代它(在这种情况下,我们显示一个常量版本,但是我们可以重载它来提供一个非常量版本),以及我们希望为`std::tuple`中的每个元素调用的函数。换句话说,这个函数将循环遍历`std::tuple`中的每个元素,并调用提供的函数,每个元素迭代一次,就像`for_each()`一样,我们习惯于在其他语言或 C++ 库中运行时使用它。 在这个函数中,我们检查是否已经达到元组的总大小。如果没有,我们获取元组中`I`当前值的元素,将其传递给提供的函数,然后用`I++ `再次调用我们的`for_each()`函数。要使用此`for_each()`功能,我们可以执行以下操作: ```cpp template void foo(Args &&...args) { std::tuple t(std::forward(args)...); for_each(t, [](const auto &arg) { std::cout << arg; }); } ``` 这里,我们得到了一个变量参数列表,我们希望遍历这个列表,并将每个参数输出到`stdout`。为此,我们像以前一样创建`std::tuple`,但这次,我们将`std::tuple`传递给我们的`for_each()`功能: ```cpp int main(void) { foo("The answer is: ", 42); std::cout << '\n'; return 0; } ``` 输出如下: ![](img/d6d45daa-3d12-43cd-b947-93a9aa3990f1.png) 就像我们在前面的例子中所做的那样,我们用一些我们希望输出到`stdout`的文本调用我们的`foo`函数,因此演示了如何使用`std:tuple`处理变量函数参数,即使我们不知道我们将被给出的参数总数。 # 使用类型特征控制函数和对象 创建 C++ 11 时,C++ 必须处理的一个问题是如何处理调整`std::vector`的大小,T0 可以采用任何类型,包括可以从`std::move()`抛出的类型。调整大小时,会创建新的内存,并将旧向量中的元素移动到新向量中。这非常有效,因为如果`std::move()`不能抛出,调整大小可以安全地执行,因为一旦调整大小功能开始将元素从一个数组移动到另一个数组,就不会出现错误。 然而,如果`std::move()`可以抛出,有可能在通过循环的部分路径上,可能会出现错误。然而,`resize()`函数无法将旧内存恢复正常,因为试图移动到旧内存也会引发异常。在这种情况下,`resize()`执行复制而不是移动。副本确保旧内存中有每个对象的有效副本;因此,如果抛出异常,原始数组保持不变,可以根据需要抛出异常。 在本食谱中,我们将探索如何通过使用特征改变模板类的行为来实现这一点。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来尝试此食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter04 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe05_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe05_example01 noexcept: r-value can throw: l-value > ./recipe05_example02 move move move move move -------------- copy copy copy copy copy ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... C++ 增加了一个叫做`std::move_if_noexcept()`的函数。如果移动构造函数/赋值运算符不能抛出,此函数将转换为 r 值,否则将转换为 l 值。例如,看看下面的代码: ```cpp #include struct the_answer_noexcept { the_answer_noexcept() = default; the_answer_noexcept(const the_answer_noexcept &is) noexcept { std::cout << "l-value\n"; } the_answer_noexcept(the_answer_noexcept &&is) noexcept { std::cout << "r-value\n"; } }; ``` 为此,我们将执行以下步骤: 1. 首先,我们将创建一个具有不能抛出的移动/复制构造函数的类: ```cpp struct the_answer_can_throw { the_answer_can_throw() = default; the_answer_can_throw(const the_answer_can_throw &is) { std::cout << "l-value\n"; } the_answer_can_throw(the_answer_can_throw &&is) { std::cout << "r-value\n"; } }; ``` 2. 接下来,我们将提供一个类,它有一个可以抛出的移动/复制构造函数。最后,让我们使用`std::move_if_noexcept()`来查看当试图移动上述每个类的实例时,是发生了移动还是复制: ```cpp int main(void) { the_answer_noexcept is1; the_answer_can_throw is2; std::cout << "noexcept: "; auto is3 = std::move_if_noexcept(is1); std::cout << "can throw: "; auto is4 = std::move_if_noexcept(is2); return 0; } ``` 前面代码的输出如下: ![](img/0308a282-75f7-42fd-82e6-4debaf2bd0d2.png) 如前面的示例所示,在一种情况下,调用移动构造函数,而在另一种情况下,根据执行移动时该类型是否可以引发异常来调用复制构造函数。 3. 现在,让我们创建一个带有调整大小功能的简单模拟向量,演示如何使用特征改变我们的`template`类的行为: ```cpp #include #include #include template class mock_vector { public: using size_type = std::size_t; mock_vector(size_type s) : m_size{s}, m_buffer{std::make_unique(m_size)} { } void resize(size_type size) noexcept(std::is_nothrow_move_constructible_v) { auto tmp = std::make_unique(size); for (size_type i = 0; i < m_size; i++) { tmp[i] = std::move_if_noexcept(m_buffer[i]); } m_size = size; m_buffer = std::move(tmp); } private: size_type m_size{}; std::unique_ptr m_buffer{}; }; ``` 我们的模拟向量有一个内部缓冲区和一个大小。创建向量时,我们使用给定的大小分配内部缓冲区。然后,我们提供了一个`resize()`函数,该函数可用于在给定新大小的情况下调整内部缓冲区的大小。我们要做的第一件事是创建新的内部缓冲区,然后循环遍历每个元素以及从一个缓冲区到另一个缓冲区的元素。如果`T`不能抛出,则在循环执行过程中不会触发异常,在这种情况下,新的缓冲区将有效。如果`T`能够抛出,将会出现一个副本。如果触发异常,旧缓冲区尚未被新缓冲区替换。相反,新的缓冲区将与复制的所有元素一起被删除。 为此,让我们创建一个可以引入移动构造函数/赋值运算符的类: ```cpp struct suboptimal { suboptimal() = default; suboptimal(suboptimal &&other) { *this = std::move(other); } suboptimal &operator=(suboptimal &&) { std::cout << "move\n"; return *this; } suboptimal(const suboptimal &other) { *this = other; } suboptimal &operator=(const suboptimal &) { std::cout << "copy\n"; return *this; } }; ``` 让我们也添加一个不能从移动构造函数/赋值运算符抛出的类: ```cpp struct optimal { optimal() = default; optimal(optimal &&other) noexcept { *this = std::move(other); } optimal &operator=(optimal &&) noexcept { std::cout << "move\n"; return *this; } optimal(const optimal &other) { *this = other; } optimal &operator=(const optimal &) { std::cout << "copy\n"; return *this; } }; ``` 最后,我们将使用这两个类创建一个向量,并尝试调整其大小: ```cpp int main(void) { mock_vector d1(5); mock_vector d2(5); d1.resize(10); std::cout << "--------------\n"; d2.resize(10); return 0; } ``` 前面代码的输出如下: ![](img/e9bee1c2-cc4a-4a6b-8b40-d2f08e9c28b8.png) 如前面的例子所示,当我们试图调整类的大小时,当移动不能抛出时执行移动,否则执行复制。换句话说,类的行为根据`T`类型的特征而变化。 # 学习如何实现模板 C++ 能够创建模板已经很长时间了,这允许程序员在给定类型的情况下创建类和函数的泛型实现。但是,您也可以提供非类型参数。 在 C++ 17 中,您现在可以使用`auto`来提供泛型、非类型模板参数。在这个食谱中,我们将探索如何使用这个功能。这很重要,因为它允许您在代码中创建更多的通用模板。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来尝试此食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter04 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe06_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe06_example01 The answer is: 42 > ./recipe06_example02 The answer is: 42 The answer is: 42 > ./recipe06_example03 The answer is: 42 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 在 C++ 17 之前,您可以在模板中提供非类型模板参数,但是您必须在定义中声明变量类型,如下例所示: ```cpp #include template void foo() { std::cout << "The answer is: " << answer << '\n'; } int main(void) { foo<42>(); return 0; } ``` 输出如下: ![](img/e5ae3433-362c-4d0d-a865-298472d67d5c.png) 在前面的例子中,我们创建了一个`int`类型的模板参数变量,并将该变量的值输出到`stdout`。在 C++ 17 中,我们现在可以执行以下操作: ```cpp #include template void foo() { std::cout << "The answer is: " << answer << '\n'; } int main(void) { foo<42>(); return 0; } ``` 输出如下: ![](img/dee00755-5e4b-4fe9-8067-fe5306327929.png) 如前所示,我们现在可以状态`auto`,而不必状态`int`。这允许我们创建一个可以接受多个非类型模板参数的函数。我们还可以使用类型特征来确定允许哪些非类型参数,如本例所示: ```cpp #include #include template< auto answer, std::enable_if_t, int> = 0 > void foo() { std::cout << "The answer is: " << answer << '\n'; } int main(void) { foo<42>(); return 0; } ``` 输出如下: ![](img/08ad857b-3e01-41e9-885d-ceea78bc65f1.png) 在前面的示例中,我们的模板非类型参数只能是整数类型。 # 使用显式模板声明 在本食谱中,我们将探索如何通过创建显式模板声明来加快模板类的编译。这很重要,因为模板需要编译器根据需要创建类的实例。在某些情况下,显式模板声明可以为程序员提供一种方法,通过缓存最有可能使用的模板类型来加速编译,从而避免包含模板的整个定义。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来尝试此食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter04 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe07_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe07_example01 The answer is: 42 The answer is: 42 The answer is: 42.1 > ./recipe07_example02 The answer is: 4 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 每次编译器看到使用给定类型的模板类时,它都会隐式创建该类型的一个版本。然而,这可能会发生多次,降低编译器的速度。但是,如果预期使用的类型是预先已知的,这个问题可以使用显式模板专门化来解决。看看这个例子: ```cpp #include template class the_answer { public: the_answer(T t) { std::cout << "The answer is: " << t << '\n'; } }; ``` 之前,我们创建了一个简单的结构,在构建过程中输出到`stdout`。通常,一旦看到类的第一个专门化,编译器就会创建这个类。但是,我们可以执行以下操作: ```cpp template class the_answer; template class the_answer; template class the_answer; ``` 这类似于一个类原型,它显式地创建了我们期望使用的专门化。这些必须在代码中使用之前声明(这意味着它们通常在模板定义之后声明);然而,一旦它们被陈述,它们可以如下使用: ```cpp int main(void) { the_answer{42}; the_answer{42U}; the_answer{42.1}; return 0; } ``` 代码的输出如下: ![](img/cdc10992-381a-45a6-80a6-aff500c8753f.png) 如前面的例子所示,我们可以像平常一样创建模板的实例,但是,在这种情况下,我们可以在大量使用这个类的情况下加快编译器的速度。这是因为,在源代码中,我们不需要包含模板的实现。为了演示这一点,让我们看另一个更复杂的例子。在头文件(称为`recipe07.h`)中,我们将使用以下内容创建我们的模板: ```cpp template struct the_answer { T m_answer; the_answer(T t); void print(); }; ``` 如您所见,我们有一个`template`类,它没有实现所提供的函数。然后,我们将在其自己的源文件中使用以下内容提供该模板的实现: ```cpp #include #include "recipe07.h" template the_answer::the_answer(T t) : m_answer{t} { } template void the_answer::print() { std::cout << "The answer is: " << m_answer << '\n'; } template class the_answer; ``` 正如您在前面的示例中看到的,我们添加了一个显式的模板声明。这确保了我们为期望的类生成实现。编译器将为我们期望显式创建的类创建实例,就像我们通常编写的任何其他源代码一样。不同的是,我们可以为我们想要的任何类型显式定义这个类。最后,我们将这段代码称为: ```cpp #include "recipe07.h" int main(void) { the_answer is{42}; is.print(); return 0; } ``` 输出如下: ![](img/b30b50ca-8f48-4791-80c2-83b5886b15f3.png) 如您所见,如果用显式类型定义类,而不是使用普通头文件的模板类,我们可以用同样的方式调用我们的类,普通头文件很小,没有完整的实现,允许编译器加快速度。 ================================================ FILE: docs/adv-cpp-prog-cb/05.md ================================================ # 五、并发和同步 在本章中,我们将学习如何在 C++ 中正确处理并发、同步和并行。在这里,您必须具备 C++ 和 C++ 线程的一般知识。本章很重要,因为使用 C++ 通常需要使用共享资源,如果没有正确实现线程安全,共享资源很容易被破坏。我们将从`std::mutexes`的广泛概述开始,它提供了一种同步 C++ 线程的方法。然后,我们将研究原子数据类型,它为安全处理并行性提供了另一种机制。 本章有一些菜谱,演示了如何在使用 C++ 线程时处理不同的场景,包括处理`const &`、线程安全包装、阻塞与异步编程以及 C++ 承诺和未来。这一点很重要,因为这一知识在处理多个执行线程时至关重要。 本章涵盖以下配方: * 使用互斥体 * 使用原子数据类型 * 理解`const &`可变性在多线程环境中的含义 * 使类线程安全 * 同步包装器以及如何实现它们 * 阻塞操作与异步编程 * 带着承诺和未来工作 # 技术要求 要编译和运行本章中的示例,您必须拥有运行 Ubuntu 18.04 的计算机的管理权限,并且具有功能性互联网连接。在运行这些示例之前,您必须安装以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 如果这安装在 Ubuntu 18.04 以外的任何操作系统上,则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。 # 使用互斥体 在这个食谱中,我们将学习为什么以及如何在 C++ 中使用互斥体。在 C++ 中使用多线程时,建立线程间共享的资源是很常见的。正如我们将在本食谱中演示的那样,试图同时使用这些共享资源会导致能够破坏资源的竞争条件。 互斥体(在 C++ 中,这被写成`std::mutex`)是一个用于保护共享资源的对象,确保多个线程可以以受控的方式访问共享资源。这可以防止它变得腐败。 # 准备好 在我们开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来尝试此食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter05 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe01_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe01_example01 The answer is: 42 The answer is: 42 The answer is: 42 The answer is: 42 The answer is: 42 ... > ./recipe01_example02 The answer is: 42 The answer is: 42 The answer is: 42 The answer is: 42 The answer is: 42 ... > ./recipe01_example03 ... > ./recipe01_example04 The answer is: 42 > ./recipe01_example05 The answer is: 42 The answer is: 42 The answer is: 42 The answer is: 42 The answer is: 42 ... > ./recipe01_example06 The answer is: 42 The answer is: 42 > ./recipe01_example07 > ./recipe01_example08 lock acquired lock failed ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 在本食谱中,我们将学习如何使用`std::mutex`来保护共享资源不被破坏。首先,让我们先回顾一下当多个线程同时访问资源时,资源是如何变得损坏的: ```cpp #include #include #include void foo() { static std::string msg{"The answer is: 42\n"}; while(true) { for (const auto &c : msg) { std::clog << c; } } } int main(void) { std::thread t1{foo}; std::thread t2{foo}; t1.join(); t2.join(); // Never reached return 0; } ``` 执行时,我们会得到以下输出: ![](img/01192c95-b3c1-4df5-a5a4-b94be4b18090.png) 在前面的例子中,我们创建了一个函数,在一个循环中输出到`stdout`。然后我们创建两个线程,每个线程执行前面定义的函数。正如您所看到的,当两个线程都执行时,结果输出会损坏。这是因为当一个线程正在将其文本输出到`stdout`时,另一个线程同时输出到`stdout`,导致一个线程的输出与另一个线程的输出混合。 为了处理这个问题,我们必须确保,一旦其中一个线程试图将其文本输出到`stdout`,应该允许它在另一个线程能够输出之前完成其输出。换句话说,每个线程必须轮流输出到`stdout`。当一个线程正在输出时,另一个线程必须等待轮到它。为此,我们将利用一个`std::mutex`对象。 # std::互斥 互斥体是一个用于保护共享资源的对象,以确保共享资源的使用不会导致损坏。为此,`std::mutex`具有`lock()`功能和`unlock()`功能。锁定功能*获取对共享资源(有时称为关键部分)的*访问。`unlock()` *释放*这个之前获得的权限。在另一个线程已经执行了`lock()`之后,任何执行`lock()`功能的尝试都将导致该线程必须等待`unlock()`功能被执行。 `std::mutex`如何实现取决于 CPU 的架构和操作系统;然而,一般来说,互斥体可以用一个简单的整数来实现。如果整数为`0`,`lock()`函数将整数设置为`1`并返回,告知互斥被获取。如果整数为`1`,表示互斥已经被获取,`lock()`函数会等待(即阻塞)直到整数变为`0`,然后将整数设置为`1`返回。如何实现这种等待取决于操作系统。比如`wait()`函数可以永远循环,直到整数变成`0`,称为**自旋锁**,也可以执行一个`sleep()`函数,等待一段时间,让其他线程和进程在互斥锁的同时执行。释放函数总是将整数设置为`0`,这意味着不再获取互斥体。确保互斥体正常工作的诀窍是确保使用原子操作读取/写入整数。如果使用非原子操作,整数本身将遭受互斥体试图防止的共享资源损坏。 例如,考虑以下情况: ```cpp #include #include #include #include std::mutex m{}; void foo() { static std::string msg{"The answer is: 42\n"}; while(true) { m.lock(); for (const auto &c : msg) { std::clog << c; } m.unlock(); } } int main(void) { std::thread t1{foo}; std::thread t2{foo}; t1.join(); t2.join(); // Never reached return 0; } ``` 该示例在运行时输出以下内容: ![](img/a74a32c8-e166-46cc-b84f-774e905f34cb.png) 在前面的例子中,我们创建了输出到`stdout`的相同函数。不同的是,在输出到`stdout`之前,我们通过执行`lock()`功能获取`std::mutex`。输出到`stdout`后,我们通过执行`unlock()`函数释放互斥体。`lock()`和`unlock()`功能之间的代码称为**临界区**。关键区域的任何代码在任何给定时间只能由一个线程执行,确保我们对`stdout`的使用不会被破坏。 通过控制对共享资源的访问(例如,使用互斥体)来确保共享资源不会被破坏,这称为**同步**。尽管大多数需要线程同步的场景并不复杂,但有些场景可能会产生需要整个大学课程才能覆盖的线程同步方案。由于这个原因,线程同步被认为是计算机科学中非常难以正确编程的范例。 在本食谱中,我们将介绍其中的一些场景。首先,让我们讨论一个叫做**僵局**的东西。当线程在调用`lock()`函数时进入无休止的等待状态时,就会发生死锁。死锁通常极难调试,并且是由以下几种原因造成的: * 由于程序员错误或获取互斥锁的线程崩溃,线程从未调用`unlock()` * 同一线程在调用`unlock()`之前多次调用`lock()`函数 * 每个线程以不同的顺序锁定多个互斥体 为了演示这一点,让我们看下面的例子: ```cpp #include #include std::mutex m{}; void foo() { m.lock(); } int main(void) { std::thread t1{foo}; std::thread t2{foo}; t1.join(); t2.join(); // Never reached return 0; } ``` 在前面的例子中,我们创建了两个线程,这两个线程都试图锁定互斥体,但从不调用`unlock()`。因此,第一个线程获取互斥体,然后返回而不释放它。当第二个线程试图获取互斥体时,它会被迫等待第一个线程执行`unlock()`,但它永远不会这样做,从而导致死锁(即程序永远不会返回)。 在这个例子中,死锁很容易识别和纠正;然而,在现实场景中,识别死锁要复杂得多。让我们看看下面的例子: ```cpp #include #include #include #include #include std::mutex m{}; std::array numbers{4,8,15,16,23,42}; int foo(int index) { m.lock(); auto element = numbers.at(index); m.unlock(); return element; } int main(void) { std::cout << "The answer is: " << foo(5) << '\n'; return 0; } ``` 在前面的例子中,我们编写了一个函数,在给定索引的情况下返回数组中的一个元素。此外,我们获取了一个保护数组的互斥体,并在返回之前释放互斥体。这里的挑战是我们必须`unlock()`函数可以返回的互斥体,它不仅包括从函数返回的每个可能的分支,还包括可能引发异常的所有可能情况。在上例中,如果提供的索引大于数组,`std::array`对象将引发异常,导致函数在有机会调用`unlock()`之前返回,如果另一个线程共享该数组,将导致死锁。 # 标准::锁定 _ 防护 C++ 提供了一个`std::lock_guard`对象来简化`std::mutex`对象的使用,而不是在代码中乱丢`try` / `catch`块来防止死锁,死锁假设程序员甚至能够在不出错的情况下确定每个可能发生的情况。 例如,考虑以下代码: ```cpp #include #include #include std::mutex m{}; void foo() { static std::string msg{"The answer is: 42\n"}; while(true) { std::lock_guard lock(m); for (const auto &c : msg) { std::clog << c; } } } int main(void) { std::thread t1{foo}; std::thread t2{foo}; t1.join(); t2.join(); // Never reached return 0; } ``` 执行时,我们会看到以下内容: ![](img/948ff65e-fbca-4f22-98f2-85b33fe28cea.png) 如前例所示,当我们在互斥体上正常调用`lock()`时,使用`std::lock_guard`。`std::lock_guard`在互斥体创建时调用`lock()`函数,在互斥体销毁时调用`unlock()`(一个叫做**资源获取是初始化**或 **RAII** 的成语)。无论函数如何返回(无论是从正常返回还是从异常返回),互斥体总是会被释放,确保死锁是不可能的,防止程序员必须准确地确定函数可能返回的每个可能的场景。 虽然`std::lock_guard`能够在`unlock()`从未被调用的情况下防止死锁,但是在`unlock()`被调用之前同一线程多次调用`lock()`的情况下,它不能防止死锁的发生。为了处理这种情况,C++ 提供了`std::recursive_mutex`。 # std::递归 _ 互斥 每次同一线程调用`lock()`函数时,递归互斥体都会递增存储在互斥体内的整数,而不会导致`lock()`函数等待。比如释放互斥体(即互斥体中的整数为`0`,线程`#1`调用`lock()`函数时,互斥体中的整数设置为`1`。正常情况下,如果线程`#1`再次调用`lock()`功能,`lock()`功能会看到整数为`1`,进入等待状态,直到整数设置为`0`。相反,递归互斥体将确定哪个线程正在调用`lock()`函数,如果获取互斥体的线程是调用`lock()`函数的同一个线程,互斥体中的整数将使用原子操作再次递增(现在导致`2`)。对于要释放的互斥体,线程必须调用`unlock()`,使用原子操作递减整数,直到互斥体中的整数为`0`。 递归互斥允许同一个线程想调用多少次`lock()`函数就调用多少次,防止多次调用`lock()`函数并导致死锁,代价是`lock()`和`unlock()`函数必须包含一个附加的函数调用来获取线程的`id()`实例,这样互斥就可以确定哪个线程在调用`lock()`和`unlock()`。 例如,考虑以下代码片段: ```cpp #include #include #include #include std::recursive_mutex m{}; void foo() { m.lock(); m.lock(); std::cout << "The answer is: 42\n"; m.unlock(); m.unlock(); } int main(void) { std::thread t1{foo}; std::thread t2{foo}; t1.join(); t2.join(); return 0; } ``` 前面的示例导致以下结果: ![](img/fef09e3b-fb6a-479b-a600-6c482d4c8b94.png) 在前面的例子中,我们定义了一个函数,它为递归互斥体调用`lock()`函数两次,输出到`stdout`,然后调用`unlock()`函数两次。然后我们创建两个执行这个函数的线程,导致`stdout`没有损坏,也没有死锁。 # 标准::共享 _ 互斥 到目前为止,我们的同步原语已经序列化了对共享资源的访问。也就是说,当访问关键区域时,每个线程必须一次执行一个。尽管这确保了不会发生损坏,但对于某些类型的场景来说,这是低效的。为了更好地理解这一点,我们必须首先研究是什么导致了腐败。 让我们考虑一个由两个线程同时递增的整数变量。递增整数变量的过程如下:`i = i + 1`。 让我们这样写: ```cpp int i = 0; auto tmp = i; tmp++ ; i = tmp; // i == 1 ``` 为了防止损坏,我们使用互斥来确保如果两个线程递增整数,它们会同步递增: ```cpp auto tmp_thread1 = i; tmp_thread1++ ; i = tmp_thread1; // i == 1 auto tmp_thread2 = i; tmp_thread2++ ; i = tmp_thread2; // i == 2 ``` 当这些操作混合时(也就是说,当两个操作在不同的线程中同时执行时),就会发生损坏。例如,考虑以下代码: ```cpp auto tmp_thread1 = i; // 0 auto tmp_thread2 = i; // 0 tmp_thread1++ ; // 1 tmp_thread2++ ; // 1 i = tmp_thread1; // i == 1 i = tmp_thread2; // i == 1 ``` 整数不是`2`,而是`1`,因为在第一个增量允许结束之前就读取了整数。这种情况是可能的,因为两个线程都试图写入同一个共享资源。我们称这些类型的线程为**生产者**。 但是,如果我们创建一百万个线程同时读取共享资源,会怎么样。由于整数从不改变,无论线程以什么顺序执行,它们都将读取相同的值,因此不可能损坏。我们称这些线程为**消费者**。如果我们只有消费者,我们就不需要线程同步,因为损坏是不可能的。 最后,如果我们有同样的 100 万消费者,但我们在组合中增加了一个生产者,会发生什么?现在,我们必须使用线程同步,因为生产者可能正在试图向消费者试图读取的整数写入一个值,这将导致一个损坏的结果。为了防止这种情况,我们必须使用互斥来保护整数。然而,如果我们使用`std::mutex`,所有 100 万消费者将不得不相互等待,即使消费者自己可以安全地同时执行而不用担心腐败。只有当制作人试图执行时,我们才会担心。 为了处理这个明显的性能问题,C++ 提供了`std::shared_mutex`对象。例如,考虑以下代码: ```cpp #include #include #include #include int count_rw{}; const auto &count_ro = count_rw; std::shared_mutex m{}; void reader() { while(true) { std::shared_lock lock(m); if (count_ro >= 42) { return; } } } void writer() { while(true) { std::unique_lock lock(m); if (++ count_rw == 100) { return; } } } int main(void) { std::thread t1{reader}; std::thread t2{reader}; std::thread t3{reader}; std::thread t4{reader}; std::thread t5{writer}; t1.join(); t2.join(); t3.join(); t4.join(); t5.join(); return 0; } ``` 在前面的例子中,我们创建了一个生产者函数(称为`reader`函数)和一个消费者函数(称为`writer`函数)。生产者使用`std::unique_lock()`锁定互斥,而消费者使用`std::shared_lock()`锁定互斥。每当使用`std::unique_lock()`锁定互斥体时,所有其他线程都必须等待(生产者和消费者都一样)。但是,如果使用`std::shared_lock()`锁定互斥体,则使用`std::shared_lock()`锁定互斥体的额外尝试不会导致线程等待。 只有当`std::unique_lock()`被调用时,等待才会发生。这使得消费者可以在不等待对方的情况下执行。只有当生产者试图执行时,消费者才必须等待,防止消费者相互序列化,最终导致更好的性能(特别是如果消费者的数量是 100 万)。 需要注意的是,我们使用`const`关键字来保证消费者不是生产者。这个简单的技巧确保了程序员不会意外地认为他们已经编程了一个消费者,而事实上,他们已经创建了一个生产者,因为如果发生这种情况,编译器会警告程序员。 # 标准::定时互斥 最后,我们还没有处理获取互斥锁的线程崩溃的场景。在这种情况下,任何试图获取相同互斥体的线程都将进入死锁状态,因为崩溃的线程永远没有机会调用`unlock()`。防止此问题的一种方法是使用`std::timed_mutex`。 例如,考虑以下代码: ```cpp #include #include #include std::timed_mutex m{}; void foo() { using namespace std::chrono; if (m.try_lock_for(seconds(1))) { std::cout << "lock acquired\n"; } else { std::cout << "lock failed\n"; } } int main(void) { std::thread t1{foo}; std::thread t2{foo}; t1.join(); t2.join(); return 0; } ``` 执行此操作时,我们会得到以下结果: ![](img/a606b9bb-6ef7-4885-93e6-344fc3bc06e7.png) 在前面的例子中,我们告诉 C++ 线程只允许等待 1 秒钟。如果互斥体已经被获取,并且在 1 秒钟后没有被释放,`try_lock_for()`函数将退出并返回 false,允许线程优雅地退出并处理错误,而不会进入死锁。 # 使用原子数据类型 在这个食谱中,我们将学习如何在 C++ 中使用原子数据类型。原子数据类型提供了读写简单数据类型(即布尔或整数)的能力,而不需要线程同步(即使用`std::mutex`和 friends)。为了实现这一点,原子数据类型使用特殊的中央处理器指令来实现,这些指令确保当一个操作被执行时,它是作为单个原子操作来完成的。 例如,递增一个整数可以写成如下形式: ```cpp int i = 0; auto tmp = i; tmp++ ; i = tmp; // i == 1 ``` 原子数据类型确保执行该增量,使得没有其他同时增量整数的尝试会交错,从而导致损坏。CPU 是如何做到这一点的,不在本书讨论范围之内。这是因为这在现代超标量流水线式 CPU 中极其复杂,这些 CPU 支持在多个内核和套接字上并行、无序和推测地执行指令。 # 准备好 在我们开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来尝试此食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter05 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe02_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe02_example01 count: 711 atomic count: 1000 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 在这个食谱中,我们将学习如何使用 C++ 的原子数据类型。原子数据类型仅限于简单的数据类型,如整数,由于这些数据类型实现起来极其复杂,因此只支持简单的操作,如加、减、增和减。 让我们看一个简单的例子,它不仅演示了如何在 C++ 中使用原子数据类型,还演示了为什么原子数据类型如此重要: ```cpp #include #include #include int count{}; std::atomic atomic_count{}; void foo() { do { count++ ; atomic_count++ ; } while (atomic_count < 99999); } int main(void) { std::thread t1{foo}; std::thread t2{foo}; t1.join(); t2.join(); std::cout << "count: " << count << '\n'; std::cout << "atomic count: " << atomic_count << '\n'; return 0; } ``` 当执行这段代码时,我们会得到以下结果: ![](img/216f9a16-1893-4c5f-b259-da1e2d0b4bc0.png) 在前面的例子中,我们有两个整数。第一个整数是普通的 C/C++ 整数类型,而第二个是原子数据类型(整数类型)。然后我们定义一个循环直到原子数据类型为`1000`的函数。最后,我们从两个线程执行这个函数,这意味着我们的全局整数同时增加了两个线程。 如您所见,这个简单测试的输出显示,简单的 C/C++ 整数数据类型的值与原子数据类型的值不同,但两者的增量相同。其原因可以从该功能的组装中看出(在英特尔中央处理器上),如下所示: ![](img/b1e3de9e-b754-49b6-a53a-d4e0bfd9cc2f.png) 要增加一个整数(未启用优化),编译器必须将内存内容移入寄存器,将`1`添加到寄存器中,然后将寄存器的结果写回内存。由于该代码在两个不同的线程中同时执行,因此该代码会交错,从而导致损坏。原子数据类型不会遇到同样的问题。这是因为增加原子数据类型的过程发生在单个特殊指令中,中央处理器确保在其他中央处理器上执行该指令,而不会将其内部状态与其他指令的相同内部状态交错。 原子数据类型通常用于实现同步原语,如`std::mutex`(尽管实际上`std::mutex`是使用测试和设置指令实现的,这些指令使用类似的原理,但执行速度往往比原子指令快)。这些数据类型也可以用来实现称为无锁数据结构的特殊数据结构,它能够在多线程环境中运行,而不需要`std::mutex`。无锁数据结构的好处是,在以更复杂的 CPU 硬件和其他类型的性能损失为代价处理线程同步时,没有等待状态(当 CPU 遇到原子指令时,硬件提供的大多数 CPU 优化不得不被暂时禁用)。所以,像计算机科学中的任何事情一样,他们有他们的时间和地点。 # 理解常量和可变在多线程环境中的含义 在本食谱中,我们将学习如何处理标记为`const`,但包含必须用于确保线程同步的`std::mutex`的对象。这个方法很重要,因为将`std::mutex`存储为类的私有成员很有用,但是,一旦这样做,将该对象的实例作为常量引用(即`const &`)传递将导致编译器错误。在这个食谱中,我们将演示为什么会出现这种情况,以及如何克服它。 # 准备好 在我们开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来尝试此食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter05 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe03_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe03_example01 The answer is: 42 > ./recipe03_example03 The answer is: 42 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 在这个食谱中,我们将学习如何将`std::mutex`添加到一个类的私有成员中,同时仍然能够处理`const`场景。一般来说,有两种方法可以确保对象是线程安全的。第一种方法是将`std::mutex`置于全球层面。这样做可以确保对象可以作为常量引用传递,或者对象本身可以具有标记为`const`的功能。 为此,请考虑以下代码示例: ```cpp #include #include #include std::mutex m{}; class the_answer { public: void print() const { std::lock_guard lock(m); std::cout << "The answer is: 42\n"; } }; int main(void) { the_answer is; is.print(); return 0; } ``` 在前面的例子中,我们创建了一个对象,当执行`print()`功能时,该对象输出到`stdout`。`print()`函数被标记为`const`,这告诉编译器`print()`函数不会修改任何类成员(也就是说,该函数是只读的)。由于`std::mutex`是全局的,对象的常量限定符被维护,代码编译和执行没有问题。 全局`std::mutex`对象的问题在于,该对象的每个实例都必须使用相同的`std::mutex`对象。如果用户有意这样做,这很好,但是如果您希望对象的每个实例都有自己的`std::mutex`对象(例如,当对象的同一个实例可能由多个线程执行时)怎么办? 为此,让我们使用以下示例来看看这是如何发生的: ```cpp #include #include #include class the_answer { std::mutex m{}; public: void print() const { std::lock_guard lock(m); std::cout << "The answer is: 42\n"; } }; int main(void) { the_answer is; is.print(); return 0; } ``` 如果我们试图对此进行编译,我们会得到以下结果: ![](img/944a6bd9-fba1-4f70-b061-5dc7c7c4afba.png) 在前面的例子中,我们所做的只是取前面的例子并将`std::mutex`作为私有成员移动到类内部。因此,当我们试图编译该类时,我们会得到一个编译器错误。这是因为`print()`函数被标记为`const`,这告诉编译器`print()`函数不会修改类的任何成员。问题是当你试图锁定`std::mutex`时,你必须修改它,导致编译器错误。 为了克服这一点,我们必须告诉编译器通过将`std::mutex`标记为可变来忽略这个错误。将成员标记为可变告诉编译器,即使对象作为常量引用传递或者对象定义了常量函数,也允许修改该成员。 例如,代码在标记为`mutable`的`const`上是这样出现的: ```cpp #include #include #include class the_answer { mutable std::mutex m{}; public: void print() const { std::lock_guard lock(m); std::cout << "The answer is: 42\n"; } }; int main(void) { the_answer is; is.print(); return 0; } ``` 正如您在前面的例子中所看到的,一旦我们将`std::mutex`标记为可变的,代码就会如我们所期望的那样编译和执行。需要注意的是`std::mutex`是少数可以接受使用可变的例子之一。可变关键字很容易被滥用,导致代码无法按预期编译或运行。 # 使类线程安全 在这个食谱中,我们将学习如何使一个类线程安全(也就是说,如何确保一个类的公共成员函数可以在任何时间被任何数量的线程同时调用)。大多数类,尤其是那些由 C++ 标准库提供的类,不是线程安全的,相反,假设用户将根据需要添加线程同步原语,如`std::mutex`对象。这种方法的问题在于,每个对象都有两个必须在代码中跟踪的实例:类本身及其`std::mutex`。用户还必须用使用`std::mutex`保护类的自定义版本包装每个对象的函数,导致不仅有两个对象必须被管理,而且还有一堆 C 风格的包装函数。 这个方法很重要,因为它将演示如何通过创建一个线程安全类来解决代码中的这些问题,该类将所有内容组合成一个类。 # 准备好 在我们开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来尝试此食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter05 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe04_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe04_example01 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 在这个食谱中,我们将学习如何通过实现我们自己的线程安全堆栈来创建一个线程安全类。C++ 标准库不提供线程安全的数据结构,因此,如果希望将数据结构用作跨多个线程的全局资源,可以手动添加线程安全。这可以通过实现包装函数或创建包装类来实现。 创建包装函数的优点是,对于全局对象,所需的代码量通常更小,更容易理解,而线程安全类的优点是,您可以创建类的多个实例,因为`std::mutex`是独立的。 这可以通过下面的代码示例来尝试: ```cpp #include #include #include template class my_stack { std::stack m_stack; mutable std::mutex m{}; public: template void push(ARG &&arg) { std::lock_guard lock(m); m_stack.push(std::forward(arg)); } void pop() { std::lock_guard lock(m); m_stack.pop(); } auto empty() const { std::lock_guard lock(m); return m_stack.empty(); } }; ``` 在前面的例子中,我们实现了自己的堆栈。这个栈有`std::stack`和`std::mutex`作为成员变量。然后我们重新实现`std::stack`提供的一些功能。每个函数首先尝试获取`std::mutex`,然后调用`std::stack`中的相关函数。在`push()`函数的情况下,我们利用`std::forward`来确保传递给`push()`函数的参数得到保留。 最后,我们可以像使用`std::stack`一样使用自定义堆栈。例如,看看下面的代码: ```cpp int main(void) { my_stack s; s.push(4); s.push(8); s.push(15); s.push(16); s.push(23); s.push(42); while(s.empty()) { s.pop(); } return 0; } ``` 如您所见,`std::stack`和我们的自定义堆栈唯一的区别是我们的堆栈是线程安全的。 # 同步包装器以及如何实现它们 在本食谱中,我们将学习如何制作线程安全的同步包装器。默认情况下,C++ 标准库不是线程安全的,因为不是所有的应用都需要这个功能。确保 C++ 标准库线程安全的一种机制是创建一个线程安全的类,它将您希望使用的数据结构以及`std::mutex`作为私有成员添加到类中,然后重新实现数据结构的函数,首先获取`std::mutex`,然后将函数调用转发到数据结构。这种方法的问题是,如果数据结构是一个全局资源,那么会有很多额外的代码添加到您的程序中,使得生成的代码难以阅读和维护。 这个方法很重要,因为它将演示如何通过制作线程安全的同步包装来解决代码中的这些问题。 # 准备好 在我们开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来尝试此食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter05 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe05_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe05_example01 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 在本食谱中,我们将学习如何创建线程安全的同步包装器,它允许我们将线程安全添加到 C++ 标准库数据结构中,默认情况下,这些数据结构不是线程安全的。 为此,我们将为打算使用的 C++ 标准库中的每个函数创建包装函数。这些包装函数将首先尝试获取`std::mutex`,然后将相同的函数调用转发到 C++ 标准库数据结构。 为此,请考虑下面的代码示例: ```cpp #include #include #include std::mutex m{}; template void push(S &s, T &&t) { std::lock_guard lock(m); s.push(std::forward(t)); } template void pop(S &s) { std::lock_guard lock(m); s.pop(); } template auto empty(S &s) { std::lock_guard lock(m); return s.empty(); } ``` 在前面的例子中,我们已经为`push()`、`pop()`和`empty()`函数创建了一个包装函数。这些函数试图在调用数据结构之前获取我们的全局`std::mutex`对象,在本例中,数据结构是一个模板。模板的使用创造了所谓的概念。我们的包装函数可以被任何实现`push()`、`pop()`和`empty()`的数据结构使用。另外,请注意,我们在`push()`函数中使用`std::forward`来确保被推参数的 l 值和 CV 限定符保持不变。 最后,我们可以像使用数据结构的函数一样使用包装器,唯一的区别是数据结构作为第一个参数传递。例如,看看下面的代码块: ```cpp int main(void) { std::stack mystack; push(mystack, 4); push(mystack, 8); push(mystack, 15); push(mystack, 16); push(mystack, 23); push(mystack, 42); while(empty(mystack)) { pop(mystack); } return 0; } ``` 正如您在前面的示例中看到的,我们的同步包装器的使用很简单,同时确保我们创建的堆栈现在是线程安全的。 # 阻塞操作与异步编程 在本食谱中,我们将学习阻塞操作和异步操作之间的区别。这个方法很重要,因为阻塞操作会序列化单个 CPU 上每个操作的执行。如果每个操作的执行必须以串行顺序执行,这通常没问题;但是,如果这些操作可以并行执行,异步编程可能是一种有用的优化,确保在一个操作等待的同时,其他操作仍然可以在同一个 CPU 上执行。 # 准备好 在我们开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来尝试此食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter05 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe06_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > time ./recipe06_example01 999999 999999 999999 999999 real 0m1.477s ... > time ./recipe06_example02 999999 999999 999999 999999 real 0m1.058s ... > time ./recipe06_example03 999999 999999 999998 999999 real 0m1.140s ... ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 阻塞操作是必须在下一个操作发生之前完成的操作。大多数程序是串行编写的,这意味着每条指令必须在下一条指令之前执行。然而,问题是有些操作可以并行执行(即并发或异步执行)。在最好的情况下,序列化这些操作会导致较差的性能,并且在某些情况下,如果阻塞的操作正在等待另一个永远没有机会执行的操作,则实际上会导致死锁(程序进入无休止的等待状态)。 为了演示阻塞操作,让我们检查以下内容: ```cpp #include #include #include constexpr auto size = 1000000; int main(void) { std::vector numbers1(size); std::vector numbers2(size); std::vector numbers3(size); std::vector numbers4(size); ``` 前面的代码创建了一个主函数,其中有四个`int`类型的`std::vector`对象。在以下步骤中,我们将使用这些向量来演示阻塞操作: 1. 首先,我们创建四个可以存储整数的向量: ```cpp std::generate(numbers1.begin(), numbers1.end(), []() { return rand() % size; }); std::generate(numbers2.begin(), numbers2.end(), []() { return rand() % size; }); std::generate(numbers3.begin(), numbers3.end(), []() { return rand() % size; }); std::generate(numbers4.begin(), numbers4.end(), []() { return rand() % size; }); ``` 2. 接下来,我们使用`std::generate`用随机数填充每个数组,这产生了一个带有数字和随机顺序的数组: ```cpp std::sort(numbers1.begin(), numbers1.end()); std::sort(numbers2.begin(), numbers2.end()); std::sort(numbers3.begin(), numbers3.end()); std::sort(numbers4.begin(), numbers4.end()); ``` 3. 接下来,我们对整数数组进行排序,这是本例的主要目标,因为执行此操作需要一段时间: ```cpp std::cout << numbers1.back() << '\n'; std::cout << numbers2.back() << '\n'; std::cout << numbers3.back() << '\n'; std::cout << numbers4.back() << '\n'; return 0; } ``` 4. 最后,我们输出每个数组中的最后一个条目,通常是`999999`(但不一定是,因为数字是使用随机数生成器生成的)。 前面例子的问题是操作可以并行执行,因为每个数组都是独立的。为了解决这个问题,我们可以异步执行这些操作,这意味着数组将被并行创建、填充、排序和输出。例如,考虑以下代码: ```cpp #include #include #include #include #include constexpr auto size = 1000000; int foo() { std::vector numbers(size); std::generate(numbers.begin(), numbers.end(), []() { return rand() % size; }); std::sort(numbers.begin(), numbers.end()); return numbers.back(); } ``` 我们要做的第一件事是实现一个名为`foo()`的函数,该函数创建我们的向量,用随机数填充它,对列表进行排序,并返回数组中的最后一个条目(与前面的示例相同,只是我们一次只处理一个数组,而不是`4`): ```cpp int main(void) { auto a1 = std::async(std::launch::async, foo); auto a2 = std::async(std::launch::async, foo); auto a3 = std::async(std::launch::async, foo); auto a4 = std::async(std::launch::async, foo); std::cout << a1.get() << '\n'; std::cout << a2.get() << '\n'; std::cout << a3.get() << '\n'; std::cout << a4.get() << '\n'; return 0; } ``` 然后我们使用`std::async`执行这个`foo()`函数四次,得到相同的四个数组,就像我们前面的例子一样。本例中的`std::async()`函数与手动执行四个线程的功能相同。`std::aync()`的结果是一个`std::future`对象,一旦函数完成执行,它就存储函数的结果。我们在这个例子中做的最后一件事是使用`get()`函数,一旦函数准备好,就返回它的值。 如果我们对这些函数的结果进行计时,我们可以看到异步版本比阻塞版本更快。下面的代码显示了这一点(`real`时间是寻找的时间): ![](img/46ef0e32-b06c-4bc6-9b92-5984d00d7432.png) `std::async()`函数也可以用来在同一个线程中异步执行我们的数组函数。例如,考虑以下代码: ```cpp int main(void) { auto a1 = std::async(std::launch::deferred, foo); auto a2 = std::async(std::launch::deferred, foo); auto a3 = std::async(std::launch::deferred, foo); auto a4 = std::async(std::launch::deferred, foo); std::cout << a1.get() << '\n'; std::cout << a2.get() << '\n'; std::cout << a3.get() << '\n'; std::cout << a4.get() << '\n'; return 0; } ``` 正如你在前面的例子中看到的,我们将操作从`std::launch::async`改为`std::launch::deferred`,这导致每个函数在需要函数的结果时(即调用`get()`函数时)执行一次。如果您不确定函数是否需要首先执行(也就是说,只在需要时执行函数),这很有用,缺点是程序的执行速度较慢,因为线程通常不被用作优化方法。 # 带着承诺和未来工作 在这个食谱中,我们将学习如何使用 C++ 承诺和未来。C++ `promise`是 C++ 线程的参数,而 C++ `future`是线程的返回值,可以用来手动实现`std::async`调用的相同功能。这个方法很重要,因为对`std::aync`的调用要求每个线程停止执行以获得其结果,而手动实现 C++ `promise`和 **`future`** 允许用户在线程仍在执行时获得线程的返回值。 # 准备好 在我们开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 您需要执行以下步骤来尝试此食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter05 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe07_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe07_example01 The answer is: 42 > ./recipe07_example02 The answer is: 42 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 在这个食谱中,我们将学习如何手动使用 C++ `promise`和`future`来提供一个与参数并行执行的函数,以及获取函数的返回值。首先,让我们用下面的代码来演示这是如何以最简单的形式完成的: ```cpp #include #include #include void foo(std::promise promise) { promise.set_value(42); } int main(void) { std::promise promise; auto future = promise.get_future(); std::thread t{foo, std::move(promise)}; t.join(); std::cout << "The answer is: " << future.get() << '\n'; return 0; } ``` 上面的示例在执行时会产生以下结果: ![](img/5313a9ee-d6f1-449f-90df-069c182a2a80.png) 正如您在前面的代码中看到的,C++ `promise`是线程化函数的参数。线程通过设置`promise`参数返回其值,该参数又设置了一个 C++ `future`,用户可以从它提供给线程的`promise`参数中获得该值。需要注意的是,我们使用`std::move()`来防止`promise`参数被复制(编译器会禁止,因为 C++ `promise`是一个只移动的类)。最后,我们使用`get()`函数获得线程的结果,就像使用`std::async`获得线程执行的结果一样。 手动使用`promise`和`future`的好处之一就是可以在线程完成之前得到线程的结果,让线程继续做功。例如,看看以下内容: ```cpp #include #include #include void foo(std::promise promise) { promise.set_value(42); while (true); } int main(void) { std::promise promise; auto future = promise.get_future(); std::thread t{foo, std::move(promise)}; future.wait(); std::cout << "The answer is: " << future.get() << '\n'; t.join(); // Never reached return 0; } ``` 执行时会产生以下结果: ![](img/af9f0ada-0fe3-4d17-9c75-52f61975d425.png) 在前面的例子中,我们创建了同一个线程,但是我们在线程中永远循环,这意味着线程永远不会返回。然后我们以同样的方式创建线程,但是一旦线程准备好就输出 C++ `future`的结果,我们可以使用`wait()`函数来确定。 ================================================ FILE: docs/adv-cpp-prog-cb/06.md ================================================ # 六、优化代码以提高性能 优化代码以提高性能可以确保代码充分利用 C++ 所能提供的功能。与其他高级语言不同,C++ 能够在不牺牲性能的情况下提供高级语法自由,尽管不可否认这是以更高的学习曲线为代价的。 这一章很重要,因为它将演示优化代码的更高级方法,包括如何在单元级别对软件进行基准测试,如何检查编译器为潜在优化生成的结果汇编代码,如何减少应用正在使用的内存资源数量,以及为什么像`noexcept`这样的编译器提示很重要。读完这一章,你将有能力写出更高效的 C++。 在本章中,我们将介绍以下食谱: * 对你的代码进行基准测试 * 查看汇编代码 * 减少内存分配的数量 * 声明 noexcept # 技术要求 要编译和运行本章中的示例,您必须拥有运行 Ubuntu 18.04 的计算机的管理权限,并且具有功能性互联网连接。在运行这些示例之前,您必须安装以下内容: ```cpp > sudo apt-get install build-essential git cmake valgrind ``` 如果这安装在 Ubuntu 18.04 以外的任何操作系统上,则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。 # 对你的代码进行基准测试 在这个食谱中,你将学习如何基准测试和优化你的源代码。优化源代码将产生更高效的 C++,从而延长电池寿命,提高性能,等等。这个方法很重要,因为优化源代码的过程始于确定您计划优化的资源,包括速度、内存甚至功耗。如果没有基准测试工具,就很难比较同一问题的不同方法。 C++ 程序员可以使用无数的基准测试工具(任何衡量程序单一属性的工具),包括诸如 Boost、Folly 和 Abseil 等 c++ API,以及英特尔的 vTune 等 CPU 专用工具。还有一些分析工具(任何有助于理解程序行为的工具),如 valgrind 和 gprof。在这个食谱中,我们将重点介绍其中的两个:Hayai 和 Valgrind。Hayai 提供了一个简单的微基准测试的例子,而 Valgrind 提供了一个更完整的,虽然更复杂的,动态分析/剖析工具的例子。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git valgrind cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 执行以下步骤完成该配方: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter06 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake -DCMAKE_BUILD_TYPE=Debug . > make recipe01_examples ``` 3. 编译完源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe01_example01 [==========] Running 2 benchmarks. [ RUN ] vector.push_back (10 runs, 100 iterations per run) [ DONE ] vector.push_back (0.200741 ms) ... [ RUN ] vector.emplace_back (10 runs, 100 iterations per run) [ DONE ] vector.emplace_back (0.166699 ms) ... > ./recipe01_example02 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 应用于 C++ 的最常见的优化是执行速度。为了优化 C++ 的速度,我们必须从开发解决同一问题的不同方法开始,然后对每个解决方案进行基准测试,以确定哪个解决方案执行速度最快。基准测试工具,如 GitHub 上基于 C++ 的基准测试库 Hayai,有助于做出这一决定。为了解释这一点,让我们看一个简单的例子: ```cpp #include #include #include std::vector data; BENCHMARK(vector, push_back, 10, 100) { data.push_back("The answer is: 42"); } BENCHMARK(vector, emplace_back, 10, 100) { data.emplace_back("The answer is: 42"); } ``` 当我们执行前面的代码时,我们得到以下输出: ![](img/4b7883ec-9592-4fde-bb6f-bcc9465077c4.jpg) 在前面的例子中,我们使用 Hayai 库来测试使用`push_back()`和`emplace_back()`向向量添加字符串之间的性能差异。`push_back()`和`emplace_back()`的区别在于`push_back()`创建对象,然后将其复制或移动到矢量中,而`emplace_back()`在矢量本身中创建对象,不需要临时对象和后续的复制/移动。也就是说,如果使用`push_back()`,必须构建一个对象,然后要么复制,要么移动到向量中。如果使用`emplace_back()`,对象构造简单。不出所料,`emplace_back()`的表现优于`push_back()`,这也是为什么铿锵-Tidy 等工具会尽可能推荐使用`emplace_back()`而不是`push_back()`的原因。 像 Hayai 这样的基准库使用简单,在帮助程序员优化源代码方面非常有效,并且不仅能够对速度进行基准测试,还能够对资源使用进行基准测试。这些库的问题在于它们在*单元*级别得到更好的利用,而不是在*集成*和*系统*级别;也就是说,为了测试整个可执行文件,这些库不太适合帮助程序员,因为随着测试规模的增加,它们不能很好地扩展。为了分析一个完整的可执行文件而不是一个单独的函数,像 Valgrind 这样的工具是存在的,它们可以帮助您分析在优化方面哪些函数最需要关注。从那里,可以使用基准测试工具来分析最需要关注的功能。 Valgrind 是一个动态分析工具,能够检测内存泄漏并跟踪程序的执行。为了看到这一点,让我们来看看下面的例子: ```cpp volatile int data = 0; void foo() { data++ ; } int main(void) { for (auto i = 0; i < 100000; i++) { foo(); } } ``` 在前面的例子中,我们从名为`foo()`的函数中增加一个全局变量(标记为 volatile,以确保编译器不会优化掉该变量),然后执行该函数`100,000`次。要分析这个例子,运行以下命令(使用`callgrind`输出每个函数在程序中被调用的次数): ```cpp > valgrind --tool=callgrind ./recipe01_example02 > callgrind_annotate callgrind.out.* ``` 这将产生以下输出: ![](img/f6aac4e5-2af5-426e-a176-9da59a0a379b.png) 如我们所见,`foo()`函数列在前一个输出的顶部附近(动态链接器的`_dl_lookup_symbol_x()`函数被调用最多,用于在执行前链接程序)。需要注意的是,程序将`foo()`功能的指令总数列为`800,000`(在左侧)。这是由于`foo()`功能是`8`装配指令长并且被执行`100,000`次。例如,让我们看看使用`objdump`的`foo()`函数的汇编(一种能够输出可执行文件的编译汇编的工具),如下所示: ![](img/f3a341c8-cf01-4265-9e74-e2496c465733.png) 使用 Valgrind,可以对可执行文件进行分析,以确定哪些函数执行时间最长。比如我们来看看`ls`: ```cpp > valgrind --tool=callgrind ls > callgrind_annotate callgrind.out.* ``` 这将产生以下输出: ![](img/050bd8f5-9adc-4825-b4db-fc8970010080.png) 我们可以看到,`strcmp`函数被调用了很多。这些信息可以与*单元*级别的基准应用编程接口相结合,以确定是否可以编写更快版本的`strcmp`(例如,使用手写汇编和特殊的中央处理器指令)。使用 Hayai 和 Valgrind 等工具,可以隔离出程序中哪些函数消耗了最多的 CPU、内存甚至电源,并重写它们以提供更好的性能,同时将精力集中在将提供最佳投资回报的优化上。 # 查看汇编代码 在本食谱中,我们将看看两种不同优化的结果程序集:循环展开和按引用传递参数。这个食谱很重要,因为它将教你如何更深入地研究编译器如何将 C++ 转换成可执行代码。这些信息将阐明为什么 C++ 规范(如 C++ 核心指南)会提出关于优化和性能的建议。当您试图编写更好的 C++ 代码时,这通常是至关重要的,尤其是当您想要优化它时。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 执行以下步骤完成该配方: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter06 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake -DCMAKE_BUILD_TYPE=Debug . > make recipe02_examples ``` 3. 编译完源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe02_example01 > ./recipe02_example02 > ./recipe02_example03 > ./recipe02_example04 > ./recipe02_example05 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 学习如何优化 C++ 代码的最好方法之一是学习如何分析编译器在编译后生成的结果汇编代码。在这个食谱中,我们将通过观察两个不同的例子来了解这个分析是如何完成的:循环展开和通过引用传递参数。 在我们看这些例子之前,让我们看一个简单的例子: ```cpp int main(void) { } ``` 在前面的例子中,我们只有一个`main()`函数。我们没有包含任何 C 或 C++ 库,`main()`函数本身是空的。如果我们编译这个例子,我们会看到生成的二进制文件仍然很大: ![](img/99ace8d4-1e3a-45be-a24f-c318040b45eb.png) 在这种情况下,示例的大小为`22kb`。为了显示编译器为此代码生成的结果程序集,我们可以执行以下操作: ```cpp > objdump -d recipe02_example01 ``` 前面命令的结果输出应该会令人惊讶,因为应用中有很多代码什么都不做。 为了更好地了解代码的真实数量,我们可以使用`grep`来细化输出,这是一个允许我们从任何命令中过滤文本的工具。让我们看看代码中的所有函数: ![](img/d17573c5-b2e5-4905-a7a5-29b065898a0d.png) 正如我们所看到的,编译器会自动为您在代码中添加几个函数。这包括`_init()`、`_fini()`和`_start()`功能。我们也可以看一个特定的函数,比如我们的主函数,如下所示: ![](img/1b25fad2-bea6-4f4c-a87b-adb0e85f399d.png) 在前面的例子中,我们在`objdump`的输出中搜索`main>:`和`RETQ`。所有函数名都以`>:`结尾,每个函数的最后一条指令(通常)是英特尔 64 位系统上的`RETQ`。 以下是生成的程序集: ```cpp 401106: push %rbp 401107: mov %rsp,%rbp ``` 首先,它将当前堆栈帧指针(`rbp`)存储到堆栈中,并为`main()`函数加载带有堆栈当前地址(`rsp`)的堆栈帧指针。 这可以在每个函数中看到,称为函数的序言。`main()`执行的唯一代码是`return 0`,由编译器自动添加到代码中: ```cpp 40110a: mov $0x0,%eax ``` 最后,这个函数中的最后一个程序集包含函数的 epilog,它恢复堆栈帧指针并返回: ```cpp 40110f: pop %rbp 401110: retq ``` 现在,我们已经更好地理解了如何获取和读取编译后的 C++ 的结果程序集,让我们来看一个循环展开的示例,这是用没有循环的指令的等效版本替换指令循环的过程。为此,请使用以下命令进行配置,确保在发布模式下编译示例(即启用编译器优化): ```cpp > cmake -DCMAKE_BUILD_TYPE=Release . > make ``` 为了理解循环展开,让我们看看下面的代码: ```cpp volatile int data[1000]; int main(void) { for (auto i = 0U; i < 1000; i++) { data[i] = 42; } } ``` 当编译器遇到循环时,它生成的结果程序集包含以下代码: ![](img/4414aa74-9275-444b-bf7a-a8a150a4a426.png) 让我们把它分解一下: ```cpp 401020: xor %eax,%eax 401022: nopw 0x0(%rax,%rax,1) ``` 前两条指令属于代码的`for (auto i = 0U;`部分。在这种情况下,`i`变量存储在`EAX`寄存器中,并使用`XOR`指令设置为`0`(英特尔的`XOR`指令比`MOV`指令更快地将寄存器设置为 0)。`NOPW`指令可以安全忽略。 接下来的几条指令是交错的,如下所示: ```cpp 401028: mov %eax,%edx 40102a: add $0x1,%eax 40102d: movl $0x2a,0x404040(,%rdx,4) ``` 这些指令代表`i++ ;`和`data[i] = 42;`代码。第一条指令存储`i`变量的当前值,然后在将`42`存储到由`i`索引的存储地址之前,将其递增 1。方便的是,这个结果程序集展示了一个可能的优化机会,因为编译器可以使用以下内容实现相同的功能: ```cpp movl $0x2a,0x404040(,%rax,4) add $0x1,%eax ``` 前面的代码在执行`i++ `之前存储了值`42`,因此不需要以下内容: ```cpp mov %eax,%edx ``` 有许多方法可以实现这种潜在的优化,包括使用不同的编译器或手写程序集。下一组指令执行我们的`for`循环的`i < 1000;`部分: ```cpp 401038: cmp $0x3e8,%eax 40103d: jne 401028 ``` `CMP`指令检查`i`变量是否为`1000`,如果不是,则使用`JNE`指令跳到函数顶部继续循环。否则,剩余的代码将执行: ```cpp 40103f: xor %eax,%eax 401041: retq ``` 为了了解循环展开是如何工作的,让我们将循环的迭代次数从`1000`更改为`4`,如下所示: ```cpp volatile int data[4]; int main(void) { for (auto i = 0U; i < 4; i++) { data[i] = 42; } } ``` 正如我们所看到的,除了循环的迭代次数之外,代码是相同的。产生的组件如下: ![](img/4414aa74-9275-444b-bf7a-a8a150a4a426.png) 我们可以看到,`CMP`和`JNE`指令缺失。现在,下面的代码被编译(*但是还有更多!*): ```cpp for (auto i = 0U; i < 4; i++) { data[i] = 42; } ``` 编译后的代码转换为以下代码: ```cpp data[0] = 42; data[1] = 42; data[2] = 42; data[3] = 42; ``` `return 0;`显示在分配之间的装配中。这是允许的,因为函数的返回值与赋值无关(因为赋值指令从不接触`RAX`,这为 CPU 提供了额外的优化(因为它可以并行执行`return 0;`,尽管这是本书范围之外的话题)。应该注意的是,循环展开不需要使用少量的循环迭代。一些编译器会部分展开一个循环来实现优化(例如,一次以`4`而不是`1`为组执行循环)。 我们的最后一个示例将关注按引用传递,而不是按值传递。要启动,请在调试模式下重新编译代码: ```cpp > cmake -DCMAKE_BUILD_TYPE=Debug . > make ``` 让我们看看下面的例子: ```cpp struct mydata { int data[100]; }; void foo(mydata d) { (void) d; } int main(void) { mydata d; foo(d); } ``` 在这个例子中,我们创建了一个大的结构,并通过值传递给我们主函数中名为`foo()`的函数。主要功能的结果组合如下: ![](img/45b46604-3410-4880-b1de-7ed7b6ecbd4b.png) 上例中的重要说明如下: ```cpp 401137: rep movsq %ds:(%rsi),%es:(%rdi) 40113a: callq 401106 <_Z3foo6mydata> ``` 前面的指令将大结构复制到堆栈中,然后调用我们的`foo()`函数。发生复制是因为结构是通过值传递的,这意味着编译器必须执行复制。另外,如果您希望看到可读格式而不是损坏格式的输出,请在选项中添加`C`,如下所示: ![](img/1e602d6c-13f7-4a30-9494-09b95deb705c.png) 最后,让我们通过引用来看看由此带来的改进: ```cpp struct mydata { int data[100]; }; void foo(mydata &d) { (void) d; } int main(void) { mydata d; foo(d); } ``` 正如我们所看到的,我们通过引用而不是通过值来传递结构。产生的组件如下: ![](img/60ea97a9-3744-48ce-bc91-000a09a842b0.png) 在这里,代码少得多,导致执行速度更快。正如我们所了解到的,如果我们希望了解编译器正在产生什么,检查编译器产生什么是有效的,因为这提供了更多关于您可以进行哪些潜在更改来编写更高效的 C++ 代码的信息。 # 减少内存分配的数量 当应用运行时,隐藏内存分配一直由 C++ 产生。这个食谱将教你如何确定 C++ 何时分配内存,以及如何在可能的情况下移除这些分配。了解如何移除内存分配很重要,因为像`new()`、`delete()`、`malloc()`和`free()`这样的函数不仅速度慢,而且它们提供的内存也是有限的。删除不需要的分配不仅可以提高应用的整体性能,还有助于降低其整体内存需求。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git valgrind cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 执行以下步骤完成该配方: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter06 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe03_examples ``` 3. 编译完源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe03_example01 > ./recipe03_example02 > ./recipe03_example03 > ./recipe03_example04 > ./recipe03_example05 > ./recipe03_example06 > ./recipe03_example07 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 在这个食谱中,我们将学习如何监控一个应用消耗了多少内存,以及 C++ 在幕后分配内存的不同方式。首先,让我们看一个简单的应用,它什么也不做: ```cpp int main(void) { } ``` 正如我们所看到的,这个应用什么也不做。要查看应用使用了多少内存,我们将使用动态分析工具 Valgrind,如下所示: ![](img/4b394af7-4501-4c48-bfd4-20e1f216d0de.png) 如上例所示,我们的应用已经分配了堆内存(即使用`new()` / `delete()`或`malloc()` / `free()`分配的内存)。为了确定这个分配发生在哪里,让我们再次使用 Valgrind,但是这一次,我们将启用一个名为 **Massif** 的工具,它将跟踪内存分配来自哪里: ![](img/def07c68-3679-40b0-ae34-b2b5ebb1757c.png) 要查看前面示例的输出,我们必须输出一个自动为我们创建的文件: ```cpp > cat massif.out.* ``` 这导致我们检索以下输出: ![](img/2434cd14-83e9-4761-b2d4-a3f3616eb879.png) 我们可以看到,动态链接器的`init()`函数正在执行分配,大小为`72,704`字节。为了进一步演示如何使用 Valgrind,让我们看一下这个简单的例子,在这里我们执行自己的分配: ```cpp int main(void) { auto ptr = new int; delete ptr; } ``` 要查看前面源代码的内存分配,我们需要再次运行 Valgrind: ![](img/5ca77107-fafd-479a-8c84-a660ea785dd3.png) 可以看到,我们已经分配了`72,708`字节。因为我们知道应用会自动为我们分配`72,704`字节,所以我们可以看到 Valgrind 已经成功检测到我们分配的`4`字节(在运行 Linux 的英特尔 64 位系统上的整数大小)。要了解这种分配发生在哪里,让我们再次使用 Massif: ![](img/d1b7826e-721d-44ad-8c3a-dbc55d4cc5e1.png) 正如我们所看到的,我们已经将`--threshold=0.1`添加到命令行选项中,因为这告诉 Valgrind,任何构成`.1%`分配的分配都应该被记录。让我们来看看结果(T3 程序只是将文件的内容回显到控制台): ```cpp > cat massif.out.* ``` 通过这样做,我们得到以下输出: ![](img/8e0189c6-c5a1-44bb-a848-4d752d4bee3a.png) 正如我们所看到的,Valgrind 已经检测到了来自`init()`函数以及我们的`main()`函数的内存分配。 现在,我们已经知道如何分析我们的应用进行的内存分配,让我们看看一些不同的 c++ API,看看它们在幕后进行什么类型的内存分配。首先,我们来看一个`std::vector`,如下: ```cpp #include std::vector data; int main(void) { for (auto i = 0; i < 10000; i++) { data.push_back(i); } } ``` 这里,我们创建了一个整数的全局向量,然后将`10,000`个整数添加到向量中。使用 Valgrind,我们得到以下输出: ![](img/01e29595-e5db-4190-8321-93c0973e49cb.png) 在这里,我们可以看到 16 个分配,总共有`203,772`字节。我们知道应用将为我们分配`72,704`字节,所以我们必须从总数中删除它,为我们留下`131,068`字节的内存。我们也知道我们分配了`10,000`整数,总共是`40,000`字节。那么,问题是,其他`91,068`字节是从哪里来的呢? 答案在于`std::vector`是如何在引擎盖下运作的。`std::vector`必须确保始终连续查看内存,这意味着当发生插入并且`std::vector`空间不足时,它必须分配一个新的、更大的缓冲区,然后将旧缓冲区的内容复制到新缓冲区中。问题是`std::vector`不知道当所有插入完成时缓冲区的总大小是多少,所以当执行第一次插入时,它会创建一个小缓冲区以确保不会浪费内存,然后随着向量的增长以小增量增加`std::vector`的大小,从而导致几个内存分配和内存副本。 为了防止这种分配的发生,C++ 提供了`reserve()`函数,该函数为用户提供了一个`std::vector`来估计用户认为他们需要多少内存。例如,考虑以下代码: ```cpp #include std::vector data; int main(void) { data.reserve(10000); // <--- added optimization for (auto i = 0; i < 10000; i++) { data.push_back(i); } } ``` 上一个例子中的代码和上一个例子中的一样,不同的是我们增加了对`reserve()`函数的调用,它告诉`std::vector`我们认为向量会有多大。Valgrind 的输出如下: ![](img/726ac6f0-8701-40ea-b53e-310902914389.png) 我们可以看到,应用分配了`112,704`字节。如果我们移除应用默认创建的`72,704`字节,我们将剩下`40,000`字节,这是我们期望的确切大小(因为我们将`10,000`整数添加到向量中,每个整数都是`4`字节大小)。 数据结构不是执行隐藏分配的唯一类型的 C++ 标准库 API。我们来看一个`std::any`,如下: ```cpp #include #include std::any data; int main(void) { data = 42; data = std::string{"The answer is: 42"}; } ``` 在这个例子中,我们创建了一个`std::any`并给它分配了一个整数和一个`std::string`。让我们看看 Valgrind 的输出: ![](img/56140dbb-bf2e-4670-82e2-15eb7134ce6d.png) 我们可以看到,`3`分配发生了。第一次分配默认发生,第二次分配由`std::string`产生。最后一次分配由`std::any`产生。出现这种情况是因为`std::any`必须调整其内部存储,以考虑其看到的任何新的随机数据类型。换句话说,为了处理一个*通用*数据类型,C++ 必须执行一个分配。如果我们不断改变数据类型,情况会变得更糟。例如,考虑以下代码: ```cpp #include #include std::any data; int main(void) { data = 42; data = std::string{"The answer is: 42"}; data = 42; // <--- keep swapping data = std::string{"The answer is: 42"}; // <--- keep swapping data = 42; // <--- keep swapping data = std::string{"The answer is: 42"}; // ... data = 42; data = std::string{"The answer is: 42"}; } ``` 前面的代码与前面的示例相同,唯一的区别是我们在数据类型之间进行了交换。Valgrind 产生以下输出: ![](img/1df2f995-397d-4929-adfd-d6847ce57abf.png) 如我们所见,`9`分配代替`3`发生。要解决这个问题,我们需要用一个`std::variant`代替`std::any`,如下: ```cpp #include #include std::variant data; int main(void) { data = 42; data = std::string{"The answer is: 42"}; } ``` `std::any`和`std::variant`的区别在于`std::variant`要求用户说明变量必须支持哪些类型,从而消除了分配时动态内存分配的需要。Valgrind 的输出如下: ![](img/2a8432c6-3aa6-4f3e-9f2d-d83c733ea3d2.png) 现在,我们只有`2`分配,正如预期的那样(默认分配和来自`std::string`的分配)。如本食谱所示,包括 C++ 标准库在内的库可以隐藏内存分配,这可能会降低代码的速度,并使用比预期更多的内存资源。像 Valgrind 这样的工具可以用来识别这些类型的问题,允许您创建更高效的 C++ 代码。 # 声明 noexcept C++ 11 引入了`noexcept`关键字,除了简化异常的一般使用方式之外,它还包括一个更好的 C++ 异常实现,该实现消除了异常的一些性能影响。然而,这并不意味着例外情况不包括*开销*(即绩效处罚)。在本食谱中,我们将探讨异常如何增加应用的开销,以及`noexcept`关键字如何帮助减少这些损失(取决于编译器)。 这个方法很重要,因为它将证明如果一个函数没有抛出异常,那么它应该被标记为异常,以防止关于应用总大小的额外开销,导致应用加载更快。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 执行以下步骤完成该配方: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter06 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe04_examples ``` 3. 编译完源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe04_example01 > ./recipe04_example02 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 在这个食谱中,我们将了解为什么如果一个函数不应该抛出异常,那么将它标记为`noexcept`是如此重要。这是因为它为应用消除了额外的异常支持开销,这可以改善执行时间、应用大小甚至加载时间(这取决于编译器、您使用的标准库等等)。为了展示这一点,让我们创建一个简单的例子: ```cpp class myclass { int answer; public: ~myclass() { answer = 42; } }; ``` 我们需要做的第一件事是创建一个类,在析构时设置一个`private`成员变量,如下所示: ```cpp void foo() { throw 42; } int main(void) { myclass c; try { foo(); } catch (...) { } } ``` 现在,我们可以创建两个函数。第一个函数抛出一个异常,而第二个函数是我们的主函数。这个函数创建了我们类的一个实例,并在一个`try` / `catch`块中调用`foo()`函数。换句话说,`main()`函数在任何时候都不会抛出异常。如果我们查看主函数的程序集,我们将看到以下内容: ![](img/e8b37484-b6b5-4a42-a49c-ace342254030.png) 如我们所见,我们的主函数调用`_Unwind_Resume`,由异常解卷器使用。这种额外的逻辑是由于 C++ 必须在函数的末尾添加额外的异常逻辑。要删除这个额外的逻辑,告诉编译器`main()`函数没有被抛出: ```cpp int main(void) noexcept { myclass c; try { foo(); } catch (...) { } } ``` 添加`noexcept`告诉编译器不能抛出异常。因此,该函数不再包含用于处理异常的额外逻辑,如下所示: ![](img/b17102a6-7e31-4c82-8751-308a935b23f2.png) 如我们所见,展开功能不再存在。需要注意的是,存在对 catch 函数的调用,这是由于`try` / `catch`块,而不是异常的开销。 ================================================ FILE: docs/adv-cpp-prog-cb/07.md ================================================ # 七、调试和测试 在本章中,您将学习如何正确测试和调试您的 C++ 应用。这一点很重要,因为如果没有良好的测试和调试,您的 C++ 应用很可能包含难以检测的错误,这会降低它们的整体可靠性、稳定性和安全性。 本章将从单元测试的全面概述开始,单元测试是在单元级别测试代码的行为,本章还将研究如何利用现有的库来加快编写测试的过程。接下来,它将演示如何使用 ASAN 和瑞银动态分析工具来检查内存损坏和未定义的行为。最后,本章将以快速查看如何在您自己的代码中利用`NDEBUG`宏在尝试解决问题时添加调试逻辑来结束。 本章包含以下配方: * 掌握单元测试 * 和 ASAN 一起工作,地址消毒剂 * 与未定义的行为消毒剂瑞银合作 * 使用`#ifndef NDEBUG`有条件地执行附加检查 # 技术要求 要编译和运行本章中的示例,您必须拥有运行 Ubuntu 18.04 的计算机的管理权限,并且具有功能性互联网连接。在运行这些示例之前,您必须安装以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 如果这安装在 Ubuntu 18.04 以外的任何操作系统上,则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。 章节的代码文件可以在[https://github . com/packt publishing/Advanced-CPP-cook book/tree/master/chapter 07](https://github.com/PacktPublishing/Advanced-CPP-CookBook/tree/master/chapter07)找到。 # 掌握单元测试 在这个食谱中,我们将学习如何单元测试我们的 C++ 代码。有几种不同的方法可以确保您的 C++ 代码以可靠、稳定、安全和符合规范的方式执行。 单元测试是在基本单元级别测试代码的行为,是任何测试策略的关键组成部分。这个食谱很重要,不仅因为它将教你如何对代码进行单元测试,还因为它将解释为什么单元测试如此关键,以及如何使用现有的库加快对 C++ 进行单元测试的过程。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 按照以下步骤完成配方: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter07 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe01_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe01_example01 =========================================================================== All tests passed (1 assertion in 1 test case) > ./recipe01_example02 =========================================================================== All tests passed (6 assertions in 1 test case) > ./recipe01_example03 =========================================================================== All tests passed (8 assertions in 1 test case) > ./recipe01_example04 =========================================================================== All tests passed (1 assertion in 1 test case) > ./recipe01_example05 ... =========================================================================== test cases: 1 | 1 passed assertions: - none - > ./recipe01_example06 ... =========================================================================== test cases: 5 | 3 passed | 2 failed assertions: 8 | 6 passed | 2 failed > ./recipe01_example07 =========================================================================== test cases: 1 | 1 passed assertions: - none - > ./recipe01_example08 =========================================================================== All tests passed (3 assertions in 1 test case) ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 简单地编写 C++ 应用,并希望它不需要任何测试就能如预期那样工作,肯定会导致可靠性、稳定性和安全性相关的错误。这个方法很重要,因为在发布前测试应用可以确保应用按预期执行,最终为您节省时间和金钱。 测试代码有几种不同的方法,包括系统级、集成、长期稳定性、静态和动态分析等。在本食谱中,我们将重点介绍**单元测试**。单元测试将一个应用分解成功能性的**单元**,并测试每个单元以确保它按预期执行。通常,在实践中,每个函数和对象(即类)都是一个应该独立测试的单元。 关于如何执行单元测试,有几种不同的理论,整本书都是关于这个主题的。一些人认为应该测试函数或对象中的每一行代码,利用覆盖工具来确保符合性,而另一些人认为单元测试应该是需求驱动的,使用黑盒方法。一个被称为**测试驱动开发**的常见开发过程规定,所有的测试,包括单元测试,都应该在任何源代码被编写之前被编写,而**行为驱动开发**通过一个特定的、故事驱动的单元测试方法,将测试驱动开发向前推进了一步。 每个测试模型都有它的优缺点,你选择哪种方法将取决于你正在编写的应用的类型,你坚持的软件开发过程的类型,以及你可能需要或不需要遵循的任何策略。不管这种选择如何,单元测试都可能是您的测试方案的一部分,这个方法将为如何单元测试您的 C++ 应用提供基础。 虽然单元测试可以用标准的 C++ 来完成(例如`libc++ `就是这样进行单元测试的),但是单元测试库有助于简化这个过程。在这个食谱中,我们将利用`Catch2`单元测试库,它可以在 [https://github.com/catchorg/Catch2.git](https://github.com/catchorg/Catch2.git)找到。 虽然我们将回顾 Catch2,但是正在讨论的原则适用于大多数可用的单元测试库,或者即使是标准的 C++,如果您选择不使用助手库的话。要利用 Catch2,只需执行以下步骤: ```cpp > git clone https://github.com/catchorg/Catch2.git catch > cd catch > mkdir build > cd build > cmake .. > make > sudo make install ``` 您也可以使用 CMake 的`ExternalProject_Add`,正如我们在 GitHub 上的例子中所做的那样,来利用库的本地副本。 为了了解如何使用 Catch2,让我们看下面这个简单的例子: ```cpp #define CATCH_CONFIG_MAIN #include TEST_CASE("the answer") { CHECK(true); } ``` 运行时,我们会看到以下输出: ![](img/82b7302a-7165-4cf8-92f1-83a6491e786f.png) 在前面的例子中,我们从定义`CATCH_CONFIG_MAIN`开始。这告诉 Catch2 库,我们希望它为我们创建`main()`函数。这必须在我们包含 Catch2 `include`语句之前定义,我们在前面的代码中已经这样做了。 下一步是定义一个测试用例。每一个单元都被分解成测试用例来测试有问题的单元。每个测试用例的粒度由您决定:一些人选择为每个被测试的单元拥有一个测试用例,而另一些人,例如,选择为每个被测试的功能拥有一个测试用例。`TEST_CASE()`接受一个字符串,该字符串允许您提供测试用例的描述,这在测试失败时很有帮助,因为 Catch2 将输出该字符串来帮助您识别测试代码中失败发生的位置。我们简单例子的最后一步是使用`CHECK()`宏。这个宏执行特定的测试。每个`TEST_CASE()`可能会有几个`CHECK()`宏,旨在为设备提供特定的输入,然后验证结果输出。 一旦编译并执行,单元测试库将提供一些描述如何执行测试的输出文本。在这种情况下,库声明所有的测试都通过了,这就是期望的结果。 为了更好地理解如何在自己的代码中利用单元测试,让我们看看下面这个更复杂的例子: ```cpp #define CATCH_CONFIG_MAIN #include #include #include #include TEST_CASE("sort a vector") { std::vector v{4, 8, 15, 16, 23, 42}; REQUIRE(v.size() == 6); SECTION("sort descending order") { std::sort(v.begin(), v.end(), std::greater()); CHECK(v.front() == 42); CHECK(v.back() == 4); } SECTION("sort ascending order") { std::sort(v.begin(), v.end(), std::less()); CHECK(v.front() == 4); CHECK(v.back() == 42); } } ``` 像前面的例子一样,我们用`CATCH_CONFIG_MAIN`宏包含 Catch2,然后用描述定义一个测试用例。在这个例子中,我们正在测试对向量进行排序的能力,所以这就是我们提供的描述。我们在测试中做的第一件事是创建一个带有预定义整数列表的整数向量。 接下来我们要做的是使用`REQUIRE()`宏进行测试,确保向量中有`6`元素。`REQUIRE()`宏与`CHECK()`类似,两者都检查以确保宏中的语句是真实的。区别在于`CHECK()`宏将报告错误,然后继续执行,而`REQUIRE()`宏将停止执行,停止单元测试。这有助于确保单元测试是基于测试可能做出的任何假设而正确构建的。`REQUIRE()`的使用很重要,因为单元测试随着时间的推移而成熟,并且其他程序员添加和修改单元测试,确保随着时间的推移 bug 不会被引入单元测试,因为没有什么比测试和调试您的单元测试更糟糕的了。 `SECTION()`宏用于用更好的描述进一步分解我们的测试,并提供为每个测试添加通用设置代码的能力。在前面的例子中,我们测试了向量的`sort()`函数。`sort()`函数可以向不同的方向排序,这是单元测试必须验证的。没有`SECTION()`宏,如果测试失败,很难知道失败是来自升序还是降序排序。此外,`SECTION()`宏确保每个测试不影响其他测试的结果。 最后,我们使用`CHECK()`宏来确保`sort()`功能按预期工作。单元测试也应该检查异常。在下面的示例中,我们将确保正确抛出异常: ```cpp #define CATCH_CONFIG_MAIN #include #include #include #include void foo(int val) { if (val != 42) { throw std::invalid_argument("The answer is: 42"); } } TEST_CASE("the answer") { CHECK_NOTHROW(foo(42)); REQUIRE_NOTHROW(foo(42)); CHECK_THROWS(foo(0)); CHECK_THROWS_AS(foo(0), std::invalid_argument); CHECK_THROWS_WITH(foo(0), "The answer is: 42"); REQUIRE_THROWS(foo(0)); REQUIRE_THROWS_AS(foo(0), std::invalid_argument); REQUIRE_THROWS_WITH(foo(0), "The answer is: 42"); } ``` 和前面的例子一样,我们定义`CATCH_CONFIG_MAIN`宏,添加我们需要的包含,并定义一个单独的`TEST_CASE()`。我们还定义了一个`foo()`函数,如果`foo()`函数的输入无效,就会抛出这个函数。 在我们的测试案例中,我们首先用有效的输入测试`foo()`函数。由于`foo()`函数没有输出(也就是说,函数返回`void`,我们通过使用`CHECK_NOTHROW()`宏确保没有抛出异常来检查以确保函数已经正确执行。需要注意的是,和`CHECK()`宏一样,`CHECK_NOTHROW()`宏也有等效的`REQUIRE_NOTHROW()`,如果检查失败将会暂停执行。 最后,我们确保`foo()`函数在其输入无效时抛出异常。有几种不同的方法可以做到这一点。`CHECK_THROWS()`宏只是确保抛出了一个异常。`CHECK_THROWS_AS()`宏确保不仅抛出了异常,而且该异常属于`std::runtime_error`类型。两者都必须为真,测试才能通过。最后,`CHECK_THROWS_WITH()`宏确保抛出了异常,并且`what()`字符串返回了我们期望的与异常匹配的结果。与其他版本的`CHECK()`宏一样,这些宏也有`REQUIRE()`版本。 虽然 Catch2 库提供了宏,可以让您深入了解每种异常类型的具体细节,但应该注意的是,除非异常类型和字符串在您的 API 要求中有明确定义,否则应该使用通用的`CHECK_THROWS()`宏——例如,`at()`函数由规范定义,当索引无效时总是返回一个`std::out_of_range`异常。在这种情况下,应该使用`CHECK_THROWS_AS()`宏来确保`at()`功能与规范相匹配。该异常返回的字符串未被指定为规范的一部分,因此应避免使用`CHECK_THROWS_WITH()`。这很重要,因为编写单元测试时的一个常见错误是编写了过度指定的单元测试。当被测试的代码被更新时,过度指定的单元测试必须经常被更新,这不仅成本高,而且容易出错。 单元测试应该足够详细,以确保单元按预期执行,但足够通用,以确保对源代码的修改不需要更新单元测试本身,除非应用编程接口的要求发生变化,导致一组单元测试老化良好,同时仍然为确保可靠性、稳定性、安全性甚至合规性提供必要的测试。 一旦有了一组单元测试来验证每个单元都按预期执行,下一步就是确保每当代码被修改时,单元测试都被执行。这可以手动完成,也可以通过**持续集成** ( **CI** )服务器自动完成,如 TravisCI 但是,当您决定这样做时,请确保单元测试返回正确的错误代码。在前面的例子中,当单元测试通过时,单元测试本身以`EXIT_SUCCESS`退出,并打印一个简单的字符串,说明所有测试都通过了。对于大多数配置项来说,这已经足够了,但是在某些情况下,让 Catch2 以易于解析的格式输出结果可能会很有用。 例如,考虑以下代码: ```cpp #define CATCH_CONFIG_MAIN #include TEST_CASE("the answer") { CHECK(true); } ``` 让我们用下面的代码来运行: ```cpp > ./recipe01_example01 -r xml ``` 如果我们这样做,我们会得到以下结果: ![](img/181d1cbf-5814-44ae-8f95-b7577da6c8e5.png) 在前面的示例中,我们创建了一个简单的测试用例(与本食谱中的第一个示例相同),并指示 Catch2 使用`-r xml`选项将测试结果输出到 XML。Catch2 有几种不同的输出格式,包括 XML 和 JSON。 除了输出格式之外,Catch2 还可以用来对我们的代码进行基准测试。例如,考虑以下代码片段: ```cpp #define CATCH_CONFIG_MAIN #define CATCH_CONFIG_ENABLE_BENCHMARKING #include #include #include TEST_CASE("the answer") { std::vector v{4, 8, 15, 16, 23, 42}; BENCHMARK("sort vector") { std::sort(v.begin(), v.end()); }; } ``` 在前面的例子中,我们创建了一个简单的测试用例,用预定义的向量编号对向量进行排序。然后,我们在一个`BENCHMARK()`宏中对这个列表进行排序,执行时会产生以下输出: ![](img/6ba0ee12-7624-4e52-897e-5182f5487f0e.png) 如上图截图所示,Catch2 多次执行该函数,平均花费`197`纳秒对向量进行排序。`BENCHMARK()`宏有助于确保代码不仅在给定特定输入的情况下以正确的输出按预期执行,而且在给定特定时间内执行。与更详细的输出格式(如 XML 或 JSON)相结合,这种类型的信息可以用来确保随着源代码的修改,生成的代码在相同的时间内或更快地执行。 为了更好地理解单元测试如何真正提高你的 C++,我们将通过两个额外的例子来结束这个食谱,这两个例子旨在提供更真实的场景。 在第一个例子中,我们将创建一个**向量**。与`std::vector`不同,在 C++ 中`std::vector`是一个动态的 C 风格的数组,数学中的向量是 *n* 维空间中的一个点(在我们的例子中,我们将其限制在 2D 空间中),其大小是该点和原点之间的距离(即 0,0)。我们在示例中实现了这个向量,如下所示: ```cpp #define CATCH_CONFIG_MAIN #include #include #include class vector { int m_x{}; int m_y{}; ``` 我们做的第一件事(除了通常的宏和 includes)是用`x`和`y`坐标定义一个类: ```cpp public: vector() = default; vector(int x, int y) : m_x{x}, m_y{y} { } auto x() const { return m_x; } auto y() const { return m_y; } void translate(const vector &p) { m_x += p.m_x; m_y += p.m_y; } auto magnitude() { auto a2 = m_x * m_x; auto b2 = m_y * m_y; return sqrt(a2 + b2); } }; ``` 接下来,我们添加一些帮助函数和构造函数。当 *x* 和 *y* 被设置为原点时,默认构造函数会生成一个没有方向或大小的向量。为了创建具有方向和大小的向量,我们还提供了另一个构造函数,允许您提供向量的初始 *x* 和 *y* 坐标。为了得到向量的方向,我们提供了 getters 返回向量的 *x* 和 *y* 值。最后,我们提供了两个助手函数。第一个辅助函数**翻译**向量,在数学中这是另一个术语,用于改变给定另一个向量的向量的 *x* 和 *y* 坐标。最后一个辅助函数返回向量的大小,如果向量的 *x* 和 *y* 值被用来构造三角形(也就是说,我们必须使用毕达哥拉斯定理来计算向量的大小),那么它就是向量斜边的长度。接下来,我们继续添加运算符,具体如下: ```cpp bool operator== (const vector &p1, const vector &p2) { return p1.x() == p2.x() && p1.y() == p2.y(); } bool operator!= (const vector &p1, const vector &p2) { return !(p1 == p2); } constexpr const vector origin; ``` 我们增加了一些等价算子,可以用来检查两个向量是否相等。我们还定义了一个表示原点的向量,这个向量的 *x* 和 *y* 值为 0。 为了测试这个向量,我们添加了以下测试: ```cpp TEST_CASE("default constructor") { vector p; CHECK(p.x() == 0); CHECK(p.y() == 0); } TEST_CASE("origin") { CHECK(vector{0, 0} == origin); CHECK(vector{1, 1} != origin); } TEST_CASE("translate") { vector p{-4, -8}; p.translate({46, 50}); CHECK(p.x() == 42); CHECK(p.y() == 42); } TEST_CASE("magnitude") { vector p(1, 1); CHECK(Approx(p.magnitude()).epsilon(0.1) == 1.4); } TEST_CASE("magnitude overflow") { vector p(INT_MAX, INT_MAX); CHECK(p.magnitude() == 65536); } ``` 第一个测试确保默认构造的向量实际上是原点。我们的下一个测试确保我们的全局**原点**向量是原点。这一点很重要,因为我们不应该假设原点是默认构造的——也就是说,未来有人可能会意外地将原点更改为`0,0`以外的其他东西。这个测试用例保证了原点实际上是`0,0`,这样以后如果有人不小心更改了这个,这个测试就会失败。由于原点必须导致 *x* 和 *y* 都为 0,因此该测试没有超出规定。 接下来,我们测试平移和幅度函数。在幅度测试的情况下,我们使用`Approx()`宏。这是必要的,因为返回的幅度是一个浮点,其大小和精度取决于硬件,与我们的测试无关。`Approx()`宏允许我们陈述精度水平,我们希望验证`magnitude()`函数的结果,该函数使用`epsilon()`修改器来实际陈述精度。在这种情况下,我们只希望验证到小数点后一位。 最后一个测试用例用于演示如何测试这些函数的所有输入。如果一个函数取整数,那么有效、无效和极端的输入都应该被测试。在这种情况下,我们正在通过 *x* 和 *y* 的`INT_MAX`。产生的`magnitude()`函数没有提供有效的结果。这是因为计算大小的过程溢出了整数类型。这种类型的错误要么应该在代码中考虑(也就是说,您应该检查可能的溢出并抛出异常),要么 API 的规范应该调用这些类型的问题(也就是说,C++ 规范可能会声明这种类型的输入的结果是未定义的)。无论哪种方式,如果一个函数取一个整数,那么所有可能的整数值都应该被测试,并且这个过程应该对所有输入类型重复。 该测试的结果如下: ![](img/64400718-2c26-405a-9f0f-f9581dec0119.png) 如前面的截图所示,该单元没有通过最后一次测试。如前所述,要解决这个问题,幅度函数应该更改为当发生溢出时抛出,找到防止溢出的方法,或者删除测试并声明此类输入未定义。 在最后一个例子中,我们将演示如何处理不返回值的函数,而是处理输入。 让我们通过创建一个写入文件的类和另一个使用第一个类将字符串写入所述文件的类来开始这个示例,如下所示: ```cpp #define CATCH_CONFIG_MAIN #include #include #include class file { std::fstream m_file{"test.txt", std::fstream::out}; public: void write(const std::string &str) { m_file.write(str.c_str(), str.length()); } }; class the_answer { public: the_answer(file &f) { f.write("The answer is: 42\n"); } }; ``` 如前面的代码所示,第一个类写入一个名为`test.txt`的文件,而第二个类将第一个类作为输入,并使用它向该文件写入一个字符串。 我们测试第二类如下: ```cpp TEST_CASE("the answer") { file f; the_answer{f}; } ``` 前面测试的问题是我们没有任何`CHECK()`宏。这是因为,除了`CHECK_NOTHROW()`,我们没有什么可查的。在这个测试中,我们正在测试以确保`the_answer{}`类调用`file{}`类并且`write()`功能正常。我们可以打开`test.txt`文件并检查以确保它是用正确的字符串编写的,但是这是一项大量的工作。这种类型的检查也是过度指定的,因为我们不是在测试`file{}`类——我们只是在测试`the_answer{}`类。如果将来我们决定`file{}`类应该写入网络文件,而不是磁盘上的文件,单元测试将不得不改变。 为了解决这个问题,我们可以利用一个叫做**嘲讽**的概念。一个`Mock`类是一个假装是输入的类的类,为单元测试提供**接缝**,允许单元测试验证测试结果。这与提供虚假输入的`Stub`不同。可悲的是,与其他语言相比,C++ 并不支持嘲讽。帮助程序库(如 GoogleMock)试图解决这个问题,但代价是要求您的所有可模拟类都包含一个 vTable(即继承纯虚拟基类),并定义每个可模拟类两次(一次在代码中,第二次在测试中,使用 Google 定义的一组 API)。这远非最佳。像希波克拉底这样的库试图解决这些问题,但代价是一些只在特定环境下有效的虚拟黑魔法,并且在出现问题时几乎不可能调试。虽然希波莫克可能是最好的选择之一(也就是说,直到 C++ 启用本机嘲讽),但以下示例是使用标准 C++ 进行嘲讽的另一种方法,唯一的缺点是冗长: ```cpp #define CATCH_CONFIG_MAIN #include #include #include class file { std::fstream m_file{"test.txt", std::fstream::out}; public: VIRTUAL ~file() = default; VIRTUAL void write(const std::string &str) { m_file.write(str.c_str(), str.length()); } }; class the_answer { public: the_answer(file &f) { f.write("The answer is: 42\n"); } }; ``` 和前面的例子一样,我们创建了两个类。第一类写入文件,而第二类使用第一类将字符串写入所述文件。不同的是我们增加了`VIRTUAL`宏。当代码被编译到我们的应用中时,`VIRTUAL`被设置为无,这意味着它被编译器从代码中移除。然而,当代码在我们的测试中编译时,它被设置为`virtual`,这告诉编译器给类一个 vTable。因为这只是在我们的测试中完成的,所以增加的开销是可以接受的。 既然我们的类在我们的测试用例中支持继承,我们可以创建我们的`file{}`类的子类版本,如下所示: ```cpp class mock_file : public file { public: void write(const std::string &str) { if (str == "The answer is: 42\n") { passed = true; } else { passed = false; } } bool passed{}; }; ``` 前面的类定义了我们的模拟。我们的模拟检查不是写入文件,而是查看是否有特定的字符串被写入我们的假文件,并根据测试结果将全局变量设置为`true`或`false`。 然后我们可以如下测试我们的`the_answer{}`类: ```cpp TEST_CASE("the answer") { mock_file f; REQUIRE(f.passed == false); f.write("The answer is not: 43\n"); REQUIRE(f.passed == false); the_answer{f}; CHECK(f.passed); } ``` 执行此操作时,我们会得到以下结果: ![](img/289a2554-23b1-4e41-9c45-6bc8b34eb163.png) 如前面的截图所示,我们现在可以检查以确保我们的类按照预期写入文件。需要注意的是,在执行我们的测试之前,我们使用`REQUIRE()`宏来确保模拟处于`false`状态。这确保了如果我们的实际测试注册为已经通过,那么它实际上已经通过,而不是因为我们的测试逻辑中的错误而注册为通过。 # 和 ASAN 一起工作,地址消毒剂 在这个食谱中,我们将学习如何利用谷歌的**地址消毒剂**(**ASAN**)——这是一个动态分析工具——来检查我们代码中的内存损坏错误。这个方法很重要,因为它提供了一种简单的方法来确保您的代码既可靠又稳定,对构建系统的更改也很少。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 按照配方执行以下步骤: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter07 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake -DCMAKE_BUILD_TYPE=ASAN .. > make recipe02_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe02_example01 ... > ./recipe02_example02 ... > ./recipe02_example03 ... > ./recipe02_example04 ... > ./recipe02_example05 ... ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能,以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 谷歌的地址消毒剂是对 GCC 和 LLVM 编译器的一组修改,以及一组在测试时必须链接到您的应用中的库。为此,我们必须在编译测试代码时添加以下编译器标志(但不要将这些标志添加到生产版本中): ```cpp -fsanitize=address -fno-optimize-sibling-calls -fsanitize-address-use-after-scope -fno-omit-frame-pointer -g -O1 ``` 这里要注意的最重要的标志是`-fsanitize=address`标志,它告诉编译器启用 ASAN。洗手液需要其余的彩旗才能正常工作,最引人注目的彩旗是`-g`和`-01`。`-g`标志启用调试,`-O1`标志将优化级别设置为 1,以提供一些性能改进。请注意,一旦启用 ASAN 工具,编译器将自动尝试链接到 ASAN 库,该库必须存在于您的计算机上。 为了演示这种消毒剂的工作原理,让我们看几个例子。 # 内存泄漏错误 `AddressSanitizer`是一个动态分析工具,旨在识别内存损坏错误。它类似于 Valgrind,但直接构建在您的可执行文件中。演示这一点最简单的例子(也是最常见的错误类型之一)是内存泄漏,如以下代码所示: ```cpp int main(void) { new int; } ``` 这将产生以下输出: ![](img/4cdecfde-c17e-47ba-b109-8dd637af2a5d.png) 在前面的例子中,我们使用`new`运算符在程序中分配一个整数,但是在退出程序之前,我们永远不会释放这个分配的内存。ASAN 工具能够检测到这个问题,并在应用完成执行时输出一个错误。 # 内存删除了两次 检测内存泄漏的能力非常有用,但这不是 ASAN 能够检测到的唯一类型的错误。另一种常见的错误是两次删除内存。例如,考虑以下代码片段: ```cpp int main(void) { auto p = new int; delete p; delete p; } ``` 执行时,我们会看到以下输出: ![](img/01d887f8-4fcc-4efc-8691-67831ec8b13a.png) 在前面的例子中,我们使用`new`运算符分配一个整数,然后使用删除运算符分配`delete`整数。由于指向先前分配的内存的指针仍然在我们的`p`变量中,我们可以再次删除它,这是我们在退出程序之前做的。在某些系统上,这将产生分段错误,因为它是未定义的行为。ASAN 工具能够检测到此问题,并输出一条错误消息,说明出现了`double-free`错误。 # 访问无效内存 另一种类型的错误是试图访问从未分配的内存。这通常是由试图取消引用空指针的代码引起的,但当指针损坏时也会发生这种情况,如下所示: ```cpp int main(void) { int *p = (int *)42; *p = 0; } ``` 这将产生以下输出: ![](img/ebd30496-bc91-49e1-b20c-1e5f580db297.png) 在前面的例子中,我们创建了一个指向整数的指针,然后为它提供了一个损坏的值`42`(这不是一个有效的指针)。然后,我们尝试取消引用损坏的指针,这将导致分段错误。应该指出的是,ASAN 工具能够发现这个问题,但它不能提供任何有用的信息。这是因为 ASAN 工具是一个连接到内存分配例程的库,跟踪每个分配以及如何使用分配。如果一个分配从未发生,它将不会有任何关于发生了什么的信息,除了典型的 Unix 信号处理程序已经能够提供的信息之外,其他动态分析工具,如 Valgrind,更适合处理这些信息。 # 删除后使用内存 为了进一步演示地址消毒器是如何工作的,让我们看下面的例子: ```cpp int main(void) { auto p = new int; delete p; *p = 0; } ``` 当我们执行此操作时,我们会看到以下内容: ![](img/c01185c7-a10c-4464-be43-c60816cfcd63.png) 前面的示例分配一个整数,然后删除该整数。然后,我们尝试使用之前删除的内存。因为这个内存位置最初是被分配的,所以 ASAN 缓存了这个地址。当取消对先前删除的内存的引用时,ASAN 能够检测到该问题为`heap-use-after-free`错误。它只能检测到这个问题,因为内存是以前分配的。 # 删除从未分配的内存 作为最后一个例子,让我们看看下面的内容: ```cpp int main(void) { int *p = (int *)42; delete p; } ``` 这将导致以下结果: ![](img/ed7e5106-c3d4-478f-8085-45a6ed4f62fb.png) 在前面的示例中,我们创建了一个指向指针的整数,然后再次为它提供了一个损坏的值。与前面的例子不同,在这个例子中,我们试图删除损坏的指针,这将导致分段错误。同样,ASAN 能够发现这个问题,但没有任何有用的信息,因为分配从未发生过。 应该注意的是,C++ 核心指南——这是现代 C++ 的编码标准——在防止我们之前描述的问题类型方面非常有帮助。具体来说,《核心指南》规定`new()`、`delete()`、`malloc()`、`free()`和好友绝对不能直接使用,而应使用`std::unique_ptr`和`std::shared_ptr`进行*所有内存分配*。这些应用编程接口自动为您分配和释放内存。如果我们再看一下前面的例子,很容易看出使用这些 API 来分配内存,而不是手动使用`new()`和`delete()`可以防止这些类型的问题发生,因为前面的大多数例子都与无效使用`new()`和`delete()`有关。 # 与未定义的行为消毒剂瑞银合作 在本食谱中,我们将学习如何在我们的 C++ 应用中使用 UBSAN 动态分析工具,该工具能够检测未定义的行为。在我们的应用中可能会引入许多不同类型的错误,未定义的行为可能是最常见的类型,因为 C 和 C++ 规范定义了几种可能出现未定义行为的情况。 这个食谱很重要,因为它将教你如何启用这个简单的功能,以及如何在你的应用中使用它。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 按照以下步骤完成配方: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter07 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake -DCMAKE_BUILD_TYPE=UBSAN . > make recipe03_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe03_example01 Floating point exception (core dumped) > ./recipe03_example02 Segmentation fault (core dumped) > ./recipe03_example03 Segmentation fault (core dumped) > ./recipe03_example04 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能,以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... UBSAN 工具能够检测几种类型的未定义行为,包括: * 越界错误 * 浮点错误 * 除以零 * 整数溢出 * 空指针取消引用 * 缺少退货 * 有符号/无符号转换错误 * 执行不到的代码 在本食谱中,我们将看几个这样的例子,但是首先,我们必须在我们的应用中启用 UBSAN 工具。为此,我们必须在应用的构建系统中启用以下标志: ```cpp -fsanitize=undefined ``` 该标志将告诉 GCC 或 LLVM 使用 UBSAN 工具,该工具为我们的应用添加了额外的逻辑以及指向 UBSAN 库的链接。应该注意的是,随着时间的推移,UBSAN 工具的功能会不断增强。因此,海湾合作委员会和 LLVM 对瑞银集团的支持程度不同。为了充分利用这个工具,您的应用应该针对 GCC 和 LLVM 进行编译,并且您应该对这两者使用最新的编译器。 # 除以零误差 用 UBSAN 演示最简单的例子之一是被零除的误差,如下所示: ```cpp int main(void) { int n = 42; int d = 0; auto f = n/d; } ``` 运行时,我们会看到以下内容: ![](img/698ed489-e92b-4080-a0dc-fb224466ddf7.png) 在前面的例子中,我们创建了两个整数(分子和分母),分母设置为`0`。然后,我们对分子和分母进行除法运算,得到一个被零除的误差,当程序崩溃时,瑞银会检测并输出该误差。 # 空指针取消引用 C++ 中更常见的问题类型是空指针取消引用,如下所示: ```cpp int main(void) { int *p = 0; *p = 42; } ``` 这将导致以下结果: ![](img/61d56d5b-161b-470f-8181-68dafa5ab7ab.png) 在前面的例子中,我们创建了一个指向整数的指针,并将其设置为`0`(即`NULL`指针)。然后我们取消引用`NULL`指针并设置它的值,导致一个分段错误,当程序崩溃时,UBSAN 能够检测到这个错误。 # 越界错误 前面的两个例子都可能是使用 Unix 信号处理程序检测到的。在下一个示例中,我们将访问一个越界的数组,这在 C++ 规范中是未定义的,并且更难检测: ```cpp int main(void) { int numbers[] = {4, 8, 15, 16, 23, 42}; numbers[10] = 0; } ``` 执行时,我们会得到以下结果: ![](img/19eaa37d-90b8-4910-bc6a-bc96ec98f7dd.png) 如前例所示,我们创建一个带有`6`元素的数组,然后尝试访问数组中的第 10 个元素,这个元素是不存在的。尝试访问数组中的这个元素不一定会产生分段错误。无论如何,瑞银能够检测到这种类型的错误,并在退出时将问题输出到`stderr`。 # 溢出错误 最后,我们还可以检测有符号整数溢出错误,这种错误在 C++ 中是未定义的,但极不可能产生崩溃,反而会导致程序进入损坏状态(通常会产生无限循环、越界错误等)。考虑以下代码: ```cpp #include int main(void) { int i = INT_MAX; i++ ; } ``` 这将导致以下结果: ![](img/0db50c0b-0249-4600-9a72-c62b3fc591b0.png) 如前面的例子所示,我们创建一个整数并将其设置为最大值。然后我们尝试增加这个整数,这通常会翻转整数的符号,这是瑞银能够检测到的一个错误。 # 使用#ifndef NDEBUG 有条件地执行附加检查 在这个食谱中,我们将学习如何利用`NDEBUG`宏,它代表*无调试*。这个方法很重要,因为大多数构建系统在编译*版本*或*产品*构建时会自动定义这个宏,这可以用来在创建这样的构建时禁用调试逻辑。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 按照以下步骤完成配方: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter07 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe04_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe04_example01 The answer is: 42 > ./recipe04_example02 recipe04_example02: /home/user/book/chapter07/recipe04.cpp:45: int main(): Assertion `42 == 0' failed. Aborted (core dumped) ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... `NDEBUG`宏源于 C,用于改变`assert()`函数的行为。`assert()`功能可以编写如下: ```cpp void __assert(int val, const char *str) { if (val == 0) { fprintf(stderr, "Assertion '%s' failed.\n", str); abort(); } } #ifndef NDEBUG #define assert(a) __assert(a, #a) #else #define assert(a) #endif ``` 如前面的代码所示,如果给`__assert()`函数一个评估为`false`的布尔值(用 C 写,这是一个等于`0`的整数),则向`stderr`输出一条错误消息,应用中止。然后使用`NDEBUG`宏来确定`assert()`函数是否存在,如果应用处于发布模式,那么所有断言逻辑都将被移除,从而减小应用的大小。使用 CMake 时,我们可以通过以下方式启用`NDEBUG`标志: ```cpp > cmake -DCMAKE_BUILD_TYPE=Release .. ``` 这将自动定义`NDEBUG`宏并启用优化。为了防止这个宏被定义,我们可以做相反的事情: ```cpp > cmake -DCMAKE_BUILD_TYPE=Debug .. ``` 前面的 CMake 代码将*而不是*定义`NDEBUG`宏,而是启用调试,并禁用大多数优化(尽管这取决于编译器)。 在我们自己的代码中,`assert`宏可以如下使用: ```cpp #include int main(void) { assert(42 == 0); } ``` 这将导致以下结果: ![](img/285a3fe5-641d-4c56-8521-9fe0e4ffbceb.png) 如前例所示,我们创建了一个应用,该应用使用`assert()`宏来检查一个 false 语句,这会导致应用中止。 虽然`assert()`功能使用了`NDEBUG`宏,但是您也可以自己使用,如下所示: ```cpp int main(void) { #ifndef NDEBUG std::cout << "The answer is: 42\n"; #endif } ``` 如前代码所示,如果应用不是在*发布*模式下编译的(即编译时命令行上没有定义`NDEBUG`宏),那么应用将输出到`stdout`。同样的逻辑可以在您的代码中使用,创建您自己的调试宏和函数,以确保您的调试逻辑在*发布*模式中被删除,允许您添加您需要的调试逻辑,而无需修改您交付给客户的最终应用。 ================================================ FILE: docs/adv-cpp-prog-cb/08.md ================================================ # 八、创建和实现您自己的容器 在本章中,您将学习如何通过利用 C++ 标准模板库已经提供的现有容器,在 C++ 中创建自己的自定义容器。这一章很重要,因为在很多情况下,您的代码会有在标准模板库容器上执行的常见操作,这些操作会在整个代码中重复(实现线程安全就是这种情况)。本章中的方法将教您如何轻松地将这些重复的代码封装到一个定制的容器中,而不必从头开始编写自己的容器,也不必用难以测试和验证的重复逻辑来乱丢代码。 在本章中,您将学习实现自定义包装容器所需的技能,能够确保`std::vector`始终保持有序。第一个食谱将教你如何创建这个包装的基本知识。第二个方法将扩展第一个方法,教你如何根据容器的运行方式重新定义容器的接口。在这种情况下,由于容器总是按照排序的顺序,您将了解为什么提供一个`push_back()`函数没有意义,即使我们正在做的只是创建一个包装器(包装器的添加改变了容器本身的概念)。在第三个食谱中,你将学习使用迭代器的技巧,以及为什么在这个例子中,只能支持`const`迭代器。最后,我们将向容器中添加几个额外的 API,以提供完整的实现。 本章中的配方如下: * 使用 std::vector 的简单包装 * 添加标准::集合应用编程接口的相关部分 * 使用迭代器 * 添加标准::矢量应用编程接口的相关部分 # 技术要求 为了编译和运行本章中的示例,读者必须拥有运行 Ubuntu 18.04 的计算机的管理权限,并且具有功能性互联网连接。在运行这些示例之前,读者必须安装以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 如果这安装在 Ubuntu 18.04 以外的任何操作系统上,则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。 章节的代码文件可以在[https://github . com/packt publishing/Advanced-CPP-cook book/tree/master/chapter 08](https://github.com/PacktPublishing/Advanced-CPP-CookBook/tree/master/chapter08)找到。 # 使用 std::vector 的简单包装 在本食谱中,我们将学习如何通过包装现有的标准模板库容器来创建自己的自定义容器,以便根据需要提供自定义功能。在后面的食谱中,我们将在这个定制容器的基础上最终创建一个基于`std::vector`的完整容器。 这个方法很重要,因为利用现有容器的代码通常伴随着公共逻辑,每次使用该容器时都会复制该逻辑。这个配方(以及这一整章)将教你如何将这个重复的逻辑封装到你自己的容器中,以便它可以被独立测试。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 通过以下步骤来尝试配方: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter08 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe01_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe01_example01 1 2 3 4 5 6 7 8 > ./recipe01_example02 1 2 3 > ./recipe01_example03 3 elements: 4 42 3 elements: 4 8 15 42 3 elements: 4 8 15 16 23 42 ``` 在下一节中,我们将逐一介绍这些例子,并解释每一个例子是做什么的,以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 在这个食谱中,我们将学习如何在`std::vector`周围创建一个简单的包装容器。大多数情况下,**标准模板库** ( **STL** )容器足以执行您的应用可能需要的任务,一般来说,应该避免创建您自己的容器,因为它们很复杂,很难正确处理。 但是,有时您可能会发现自己在容器上重复执行相同的操作。当这种情况发生时,将这些常见的操作包装到一个包装器容器中通常是有帮助的,该容器可以独立地进行单元测试,以确保容器能够按预期工作。例如,STL 容器不是线程安全的。如果您需要一个容器在每次访问该容器时都具有线程安全功能,那么您首先需要确保您对该容器拥有独占访问权限(例如,通过锁定一个`std::mutex`),然后才能进行容器操作。这种模式将在整个代码中重复出现,增加了进入死锁的机会。这个问题可以通过创建一个容器包装器来防止,该包装器为容器的每个公共成员添加一个`std::mutex`。 在这个方法中,让我们考虑一个例子,在这个例子中,我们创建了一个向量(也就是说,连续内存中的元素数组,您必须能够直接访问它),这个向量必须始终保持排序顺序。首先,我们需要一些标题: ```cpp #include #include #include ``` 为了实现我们的容器,我们将利用`std::vector`。虽然我们可以从头开始实现自己的容器,但大多数时候这是不需要的,应该避免,因为这样的任务非常耗时和复杂。我们需要`std::sort`和`iostream`的`algorithm`表头进行测试。让我们补充如下: ```cpp template< typename T, typename Compare = std::less, typename Allocator = std::allocator > class container { using vector_type = std::vector; vector_type m_v; public: ``` 容器的定义将从其模板定义开始,模板定义与`std::vector`的定义相同,只是增加了`Compare`类型,用于定义我们希望容器排序的顺序。默认情况下,容器将按升序排序,但这可以根据需要进行更改。最后,容器将有一个私有成员变量,它是这个容器包装的`std::vector`的一个实例。 为了让容器与 C++ 实用程序、模板函数甚至一些关键的语言特性一起正常运行,容器将需要定义与`std::vector`相同的别名,如下所示: ```cpp using value_type = typename vector_type::value_type; using allocator_type = typename vector_type::allocator_type; using size_type = typename vector_type::size_type; using difference_type = typename vector_type::difference_type; using const_reference = typename vector_type::const_reference; using const_pointer = typename vector_type::const_pointer; using compare_type = Compare; ``` 正如您所看到的,没有必要自己手动定义别名。相反,我们可以简单地从`std::vector`本身转发别名的声明。这种情况的例外是`compare_type`别名,因为这是我们添加到包装容器中的一个别名,它代表模板类用于最终将提供给`std::sort`的比较操作的类型。 我们也不包括引用别名的非常数版本。原因是我们的容器必须始终保持`std::vector`有序。如果我们向用户提供对存储在`std::vector`中的元素的直接写访问,用户可以将`std::vector`置于无序状态,而我们的定制容器没有能力根据需要重新排序。 接下来,让我们定义我们的构造函数(映射到`std::vector`提供的相同构造函数)。 # 默认构造函数 下面定义了我们的默认构造函数: ```cpp container() noexcept(noexcept(Allocator())) { std::cout << "1\n"; } ``` 由于`std::vector`的默认构造函数产生一个空向量,因此我们没有额外的逻辑必须添加,因为空向量是默认排序的。接下来,我们必须定义一个接受自定义分配器的构造函数。 # 自定义分配器构造函数 我们的自定义分配器构造函数定义如下: ```cpp explicit container( const Allocator &alloc ) noexcept : m_v(alloc) { std::cout << "2\n"; } ``` 和前面的构造函数一样,这个构造函数创建了一个空向量,但是有一个已经存在的分配器。 # 计算构造函数 接下来的两个构造函数允许 API 的用户设置向量的最小大小,如下所示: ```cpp container( size_type count, const T &value, const Allocator &alloc = Allocator() ) : m_v(count, value, alloc) { std::cout << "3\n"; } explicit container( size_type count, const Allocator &alloc = Allocator() ) : m_v(count, alloc) { std::cout << "4\n"; } ``` 第一个构造函数将创建`count`元素的向量,所有元素都用`value`的值初始化,而第二个构造函数将创建具有默认值的元素(例如,整数向量将被初始化为零)。 # 复制/移动构造函数 为了支持复制和移动容器的能力,我们需要实现如下的复制和移动构造函数: ```cpp container( const container &other, const Allocator &alloc ) : m_v(other.m_v, alloc) { std::cout << "5\n"; } container( container &&other ) noexcept : m_v(std::move(other.m_v)) { std::cout << "6\n"; } ``` 因为我们的自定义包装容器必须始终保持排序顺序,所以将一个容器复制或移动到另一个容器不会改变容器中元素的顺序,这意味着这些构造函数也不需要排序操作。然而,我们确实特别注意通过复制或移动我们的容器封装的内部`std::vector`来确保复制和移动正确发生。 为了完整起见,我们还提供了一个移动构造函数,它允许我们像`std::vector`一样,在提供自定义分配器的同时进行移动,如下所示: ```cpp container( container &&other, const Allocator &alloc ) : m_v(std::move(other.m_v), alloc) { std::cout << "7\n"; } ``` 接下来,我们将提供一个接受初始化列表的构造函数。 # 初始化列表构造函数 最后,我们还将添加一个接受初始值设定项列表的构造函数,如下所示: ```cpp container( std::initializer_list init, const Allocator &alloc = Allocator() ) : m_v(init, alloc) { std::sort(m_v.begin(), m_v.end(), compare_type()); std::cout << "8\n"; } ``` 如前面的代码所示,初始化列表可以以任何顺序为`std::vector`提供初始元素。因此,我们必须在向量初始化后对列表进行排序。 # 使用 让我们测试这个容器,以确保每个构造函数都按预期工作: ```cpp int main(void) { auto alloc = std::allocator(); container c1; container c2(alloc); container c3(42, 42); container c4(42); container c5(c1, alloc); container c6(std::move(c1)); container c7(std::move(c2), alloc); container c8{4, 42, 15, 8, 23, 16}; return 0; } ``` 如前面的代码块所示,我们通过调用每个构造函数来测试我们的构造函数,这将产生以下输出: ![](img/d05b7686-5517-4965-80e9-17420fc8564b.png) 如您所见,每个构造函数都按预期成功执行了。 # 向我们的容器中添加元素 有了我们的构造函数,我们还需要提供手动向容器添加数据的能力(例如,如果我们最初使用默认构造函数创建了容器)。 首先,让我们关注一下`std::vector`提供的`push_back()`功能: ```cpp void push_back(const T &value) { m_v.push_back(value); std::sort(m_v.begin(), m_v.end(), compare_type()); std::cout << "1\n"; } void push_back(T &&value) { m_v.push_back(std::move(value)); std::sort(m_v.begin(), m_v.end(), compare_type()); std::cout << "2\n"; } ``` 如前面的代码片段所示,`push_back()`函数具有与版本`std::vector`相同的函数签名,允许我们简单地将函数调用转发给`std::vector`。问题是,将一个值推到`std::vector`的末尾可能会导致`std::vector`进入无序状态,要求我们在每次推送时对`std::vector`进行重新排序(要求`std::vector`始终保持有序的结果)。 解决这个问题的一种方法是向容器包装器添加另一个成员变量,该变量跟踪`std::vector`何时被污染。实现这些功能的另一种方法是按排序顺序添加元素(也就是说,遍历向量排序顺序并将元素放在适当的位置,根据需要移动剩余的元素)。如果元素很少被添加到`std::vector`中,那么这种方法可能会优于调用`std::sort`。然而,如果元素被大量添加到`std::vector`中,那么受污染的方法可能会表现得更好。 创建容器包装器的一个主要好处是,这些类型的优化可以在不改变依赖于容器本身的代码的情况下实现和测试。两种实现(或其他)都可以被实现、测试和比较,以确定哪种优化最适合您的特定需求,而使用容器的代码永远不会改变。这不仅清理了代码,而且增加的封装触及了面向对象设计的核心,确保代码中的每个对象只有一个目的。在容器包装器的情况下,目的是封装以排序顺序维护`std::vector`的操作。 为了完整起见,我们还会增加`push_back()`的`emplace_back()`版本,就像`std::vector`一样: ```cpp template void emplace_back(Args&&... args) { m_v.emplace_back(std::forward(args)...); std::sort(m_v.begin(), m_v.end(), compare_type()); std::cout << "3\n"; } ``` 与等效的`std::vector`相比,`emplace_back()`函数的不同之处在于,我们的版本不返回对所创建元素的引用。这是因为排序会使引用无效,从而无法返回有效的引用。 # 推送/定位的使用 最后,让我们测试一下我们的`push_back()`和`emplace`函数,以确保它们被正确调用,如下所示: ```cpp int main(void) { int i = 42; container c; c.push_back(i); c.push_back(std::move(i)); c.emplace_back(42); return 0; } ``` 如前面的代码片段所示,我们调用每个版本的`push_back()`以及`emplace_back()`函数,以确保它们按照预期被正确调用,结果如下: ![](img/51e13fb0-f3e4-460b-8109-137c99e246ed.png) 我们可以更进一步,向我们的测试容器添加更好的测试数据,如下所示: ```cpp int main(void) { int i = 42; container c; c.emplace_back(4); c.push_back(i); c.emplace_back(15); c.push_back(8); c.emplace_back(23); c.push_back(std::move(16)); return 0; } ``` 如前面的代码片段所示,我们将整数`4`、`42`、`15`、`8`、`23`和`16`添加到向量中。在下一个配方中,我们将从`std::set`中窃取 API,以向我们的容器提供更好的`push`和`emplace`API,以及一个输出函数,以更好地了解`std::vector`包含什么以及它包含元素的顺序。 # 添加标准::集合应用编程接口的相关部分 在这个食谱中,我们将学习如何将`std::set`中的 API 添加到我们在第一个食谱中创建的自定义容器中。具体来说,我们将了解为什么`std::vector::push_back()`和`std::vector::emplace_back()`在与我们的自定义容器一起使用时没有意义,该容器总是以排序顺序维护其内部元素。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 通过以下步骤来尝试配方: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter08 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe02_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe02_example01 elements: 4 elements: 4 42 elements: 4 15 42 elements: 4 8 15 42 elements: 4 8 15 23 42 elements: 4 8 15 16 23 42 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 在本章的第一个食谱中,我们创建了一个定制的容器包装器,它模仿了一个`std::vector`,但是它确保了向量中的元素始终保持有序的顺序,包括添加`std::vector::push_back()`函数和`std::vector::emplace_back()`函数。在本食谱中,我们将把`std::set::insert()`和`std::set::emplace()`功能添加到我们的定制容器中。 因为我们的容器包装器总是确保`std::vector`是有序的,所以在向量的前面、后面或中间添加一个元素没有区别。无论元素添加到向量的哪个位置,都必须在访问向量之前对其进行排序,这意味着无论元素添加到哪个位置,元素的添加顺序都可能发生变化。 这种对元素添加位置的不关心类似于`std::set`。`std::set`向集合中添加元素,然后返回`true`或`false`,这取决于被测试的元素是否是集合的成员。`std::set`提供`insert()`和`emplace()`功能,为器械包添加元素。让我们将这些相同的 API 添加到自定义容器中,如下所示: ```cpp void insert(const T &value) { push_back(value); } void insert(T &&value) { push_back(std::move(value)); } template void emplace(Args&&... args) { emplace_back(std::forward(args)...); } ``` 正如您在前面的代码片段中看到的,我们添加了一个`insert()`函数(复制和移动),以及一个`emplace()`函数,它只不过是调用它们的`push_back()`和`emplace_back()`等价物,确保传递给这些函数的参数被正确转发。这些应用编程接口和我们在之前的配方*中添加的应用编程接口之间的唯一区别是函数本身的名称,该配方使用了一个简单的 std::vector* 包装器。 虽然这样的改变看起来微不足道,但这很重要,因为它重新定义了容器的 API 和用户之间的概念。`push_back()`和`emplace_back()`函数建议将元素添加到向量的后面,而事实上并不是。相反,它们被简单地添加到`std::vector`中,并且`std::vector`的顺序根据所添加元素的值而改变。因此,`push_back()`和`emplace_back()`功能是需要的,但是应该要么重命名,要么标记为私有,以确保用户只使用`insert()`和`emplace()`版本来正确管理期望。当编写您自己的容器(甚至是包装器)时,坚持最少惊讶的原则是很重要的,这确保了用户正在使用的应用编程接口将按照应用编程接口可能建议的方式工作。 # 使用迭代器 在本食谱中,我们将学习如何向我们在第一个食谱中开始的自定义容器添加迭代器支持,该容器包装了一个`std::vector`,确保其内容始终保持排序顺序。 为了增加迭代器支持,我们将学习如何转发`std::vector`已经提供的迭代器(我们不会从头实现迭代器,因为这是本书范围之外的一个主题,因为从头实现容器非常困难)。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 你需要通过以下步骤来尝试这个食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter08 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe03_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe03_example01 elements: 4 8 15 16 23 42 > ./recipe03_example02 elements: 4 8 15 16 23 42 elements: 4 8 15 16 23 42 elements: 42 23 16 15 8 4 elements: 1 4 8 15 16 23 42 elements: 4 8 15 16 23 42 elements: ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 我们的定制容器包装的`std::vector`已经提供了迭代器的有效实现,可以用于我们的容器。然而,我们需要转发`std::vector`提供的 API 的特定部分,以确保迭代器正常工作,包括关键的 C++ 特性,例如基于范围的循环。 首先,让我们将最后一个剩余的构造函数`std::vector`添加到我们的自定义容器中: ```cpp template container( Iter first, Iter last, const Allocator &alloc = Allocator() ) : m_v(first, last, alloc) { std::sort(m_v.begin(), m_v.end(), compare_type()); } ``` 如前面的代码片段所示,我们给定的迭代器类型没有定义。迭代器可以来自我们容器的另一个实例,也可以直接来自`std::vector`,它不按排序顺序存储元素。即使迭代器来自我们定制容器的实例,迭代器存储元素的顺序也可能与容器元素的顺序不同。因此,我们必须在`std::vector`初始化后对其进行排序。 除了构造,重要的是我们的定制容器还包括`std::vector`提供的基于迭代器的别名,因为这些别名是容器正确使用 c++ API 所必需的。下面是它的示例代码片段: ```cpp using const_iterator = typename vector_type::const_iterator; using const_reverse_iterator = typename vector_type::const_reverse_iterator; ``` 如前面的代码片段所示,像第一个配方中定义的别名一样,我们只需要转发声明`std::vector`已经提供的别名,这样我们的定制容器也可以利用它们。不同的是,我们不包括这些迭代器别名的非常数版本。由于我们的自定义容器必须始终保持数据的排序顺序,因此我们必须限制用户直接修改迭代器内容的能力,因为这可能会导致容器元素的顺序发生变化,而我们的容器无法根据需要重新排序。相反,应通过使用`insert()`、`emplace()`和`erase()`对容器进行修改。 C++ template-based functions rely on these aliases to properly implement their features, which also include range-based for loops. 最后,`std::vector`提供的一系列基于迭代器的成员函数也应该通过我们的定制容器转发。下面的代码描述了这一点: ```cpp const_iterator begin() const noexcept { return m_v.begin(); } const_iterator cbegin() const noexcept { return m_v.cbegin(); } ``` 第一组成员函数是`begin()`函数,它提供了一个迭代器来表示`std::vector`中的第一个元素。与别名一样,我们不转发这些成员函数的非常数版本。此外,为了完整起见,我们包括了这些函数的`c`版本。在 C++ 17 中,这些是可选的,因为如果愿意,您可以使用`std::as_const()`来代替。下一组迭代器是`end()`迭代器,它提供了一个表示`std::vector`结尾的迭代器(不要与表示`std::vector`中最后一个元素的迭代器混淆)。下面的代码显示了这一点: ```cpp const_iterator end() const noexcept { return m_v.end(); } const_iterator cend() const noexcept { return m_v.cend(); } ``` 如前面的代码片段所示,和大多数成员函数一样,我们只需要将 API 转发到我们的定制容器封装的私有`std::vector`中。这个相同的过程可以对`rbegin()`和`rend()`重复,它们提供了与前面相同的 API,但是返回一个反向迭代器,它以相反的顺序遍历`std::vector`。 接下来,我们实现基于迭代器的`emplace()`函数,如下所示: ```cpp template void emplace(const_iterator pos, Args&&... args) { m_v.emplace(pos, std::forward(args)...); std::sort(m_v.begin(), m_v.end(), compare_type()); } ``` 虽然提供`emplace()` API 提供了一个更完整的实现,但是应该注意的是,只有进一步优化以利用元素添加到容器的方式中的预期位置,它才会有用。这与对`std::vector`进行排序的更好方法相结合。 虽然前面的实现是可行的,但是它的性能很可能类似于我们在第一个配方中实现的`emplace()`版本。由于自定义容器始终保持排序顺序,元素插入到`std::vector`的位置无关紧要,因为`std::vector`的新顺序将改变被添加元素的位置。这是当然的,除非 position 参数的添加为 API 提供了一些额外的支持来更好地优化添加,而我们的实现并没有这样做。因此,除非`pos`参数用于优化,否则前面的函数可能是多余和不必要的。 与前面的`emplace()`函数一样,我们不试图返回表示添加到容器中的元素的迭代器,因为这个迭代器在排序后变得无效,并且没有足够的关于添加到`std::vector`中的内容的信息来重新定位迭代器(例如,如果存在重复,就无法知道哪个元素实际上刚刚被添加)。 最后,我们实现`erase`功能,如下所示: ```cpp const_iterator erase(const_iterator pos) { return m_v.erase(pos); } const_iterator erase(const_iterator first, const_iterator last) { return m_v.erase(first, last); } ``` 与`emplace()`功能不同,从`std::vector`中移除元素不会改变`std::vector`的顺序,因此不需要排序。还需要注意的是,我们版本的`erase()`功能返回了`const`版本。这再次是因为我们不能支持迭代器的非常数版本。 最后,现在我们已经能够访问存储在容器中的元素,让我们创建一些测试逻辑来确保我们的容器按照预期工作: ```cpp int main(void) { container c{4, 42, 15, 8, 23, 16}; ``` 首先,我们将从初始值设定项列表中创建一个容器,其中包含没有顺序的整数。在这个容器被创建之后,存储这些元素的`std::vector`应该是有序的。为了证明这一点,让我们遍历容器并输出结果: ```cpp std::cout << "elements: "; for (const auto &elem : c) { std::cout << elem << ' '; } std::cout << '\n'; ``` 如前面的代码片段所示,我们首先向`stdout`输出一个标签,然后使用基于范围的 for 循环迭代我们的容器,一次输出一个元素。最后,在所有元素都输出到`stdout`后,我们输出一个新的行,结果如下: ```cpp elements: 4 8 15 16 23 42 ``` 正如预期的那样,该输出按排序顺序排列。 需要注意的是,我们的 for 循环范围必须将每个元素定义为`const`。这是因为我们不支持迭代器的非常数版本。任何使用这些迭代器的非常数版本的尝试都会导致编译器错误,如下例所示: ```cpp for (auto &elem : c) { elem = 42; } ``` 前面的代码导致以下编译器错误(这是有意的): ```cpp /home/user/book/chapter08/recipe03.cpp: In function ‘int main()’: /home/user/book/chapter08/recipe03.cpp:396:14: error: assignment of read-only reference ‘elem’ 396 | elem = 42; ``` 发生此编译器错误的原因是因为 for 循环的范围也可以写成如下形式: ```cpp std::cout << "elements: "; for (auto iter = c.begin(); iter != c.end(); iter++) { auto &elem = *iter; std::cout << elem << ' '; } std::cout << '\n'; ``` 如前面的代码片段所示,元素没有被标记为`const`,因为 ranged for 循环使用了`begin()`和`end()`成员函数,从而产生了读写迭代器(除非您明确声明`const`)。 我们还可以为新的`emplace()`函数创建一个测试,如下所示: ```cpp c.emplace(c.cend(), 1); std::cout << "elements: "; for (const auto &elem : c) { std::cout << elem << ' '; } std::cout << '\n'; ``` 这将产生以下输出: ```cpp elements: 1 4 8 15 16 23 42 ``` 如前面的输出所示,数字`1`按照排序顺序添加到我们的容器中,尽管我们告诉容器将我们的元素添加到`std::vector`的末尾。 我们还可以颠倒前面的操作,验证我们的`erase()`功能是否正常工作,如下所示: ```cpp c.erase(c.cbegin()); std::cout << "elements: "; for (const auto &elem : c) { std::cout << elem << ' '; } std::cout << '\n'; ``` 这将产生以下输出: ```cpp elements: 4 8 15 16 23 42 ``` 可以看到,新增的`1`已经成功移除。 # 添加标准::矢量应用编程接口的相关部分 在这个食谱中,我们将通过添加`std::vector`已经提供的剩余 API 来完成我们在本章前三个食谱中构建的定制容器。在这个过程中,我们将删除没有意义的或者我们不能支持的 API,因为我们的定制容器必须将`std::vector`中的元素保持在有序的顺序中。 这个方法很重要,因为它将向您展示如何正确地创建一个包装容器,该容器可以用容器所需的逻辑(例如,线程安全,或者在我们的例子中,元素顺序)来封装现有的容器。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 通过以下步骤来尝试配方: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter08 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe04_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe04_example01 elements: 4 8 15 16 23 42 elements: 4 8 15 16 23 42 elements: 4 8 15 16 23 42 elements: 42 elements: 4 8 15 16 23 42 elements: 4 8 15 16 23 42 c1.at(0): 4 c1.front(): 4 c1.back(): 42 c1.data(): 0xc01eb0 c1.empty(): 0 c1.size(): 6 c1.max_size(): 2305843009213693951 c1.capacity(): 42 c1.capacity(): 6 c1.size(): 0 c1.size(): 42 c1.size(): 0 c1.size(): 42 elements: 4 8 15 16 23 ==: 0 !=: 1 <: 1 <=: 1 >: 0 >=: 0 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 目前,我们的定制容器能够被构建、添加、迭代和删除。然而,容器不支持直接访问容器的能力,也不支持简单的操作,例如`std::move()`或比较。为了解决这些问题,让我们从添加缺失的`operator=()`重载开始: ```cpp constexpr container &operator=(const container &other) { m_v = other.m_v; return *this; } constexpr container &operator=(container &&other) noexcept { m_v = std::move(other.m_v); return *this; } ``` 第一个`operator=()`重载为复制分配提供支持,而第二个重载为移动分配提供支持。因为我们只有一个已经提供了适当的复制和移动语义的私有成员变量,所以我们不需要担心自我赋值(或移动),因为`std::vector`函数的复制和移动实现将为我们处理这一点。 如果您自己的定制容器有额外的私有元素,那么很可能需要自我赋值检查。例如,考虑以下代码: ```cpp constexpr container &operator=(container &&other) noexcept { if (&other == this) { return *this; } m_v = std::move(other.m_v); m_something = other.m_something; return *this; } ``` 剩余的`operator=()`重载取一个初始化列表,如下所示: ```cpp constexpr container &operator=(std::initializer_list list) { m_v = list; std::sort(m_v.begin(), m_v.end(), compare_type()); return *this; } ``` 如前面的代码片段所示,像初始化列表构造函数一样,我们必须在赋值后对`std::vector`重新排序,因为初始化列表可以以任何顺序提供。 接下来要实现的成员函数是`assign()`函数。下面的代码片段显示了这一点: ```cpp constexpr void assign(size_type count, const T &value) { m_v.assign(count, value); } template constexpr void assign(Iter first, Iter last) { m_v.assign(first, last); std::sort(m_v.begin(), m_v.end(), compare_type()); } constexpr void assign(std::initializer_list list) { m_v.assign(list); std::sort(m_v.begin(), m_v.end(), compare_type()); } ``` 这些函数类似于`operator=()`重载,但不提供返回值或支持附加功能。让我们看看如何: * 第一个`assign()`功能用特定的`value`计数次数填充`std::vector`。由于该值从不改变,`std::vector`将始终按排序顺序排列,在这种情况下,无需对列表进行排序。 * 第二个`assign()`函数取一个迭代器范围,类似于这个函数的构造器版本。像那个函数一样,传递给这个函数的迭代器可能来自原始的`std::vector`或者我们定制容器的另一个实例,但是排序顺序不同。为此,我们必须在作业后整理`std::vector`。 * 最后,`assign()`函数还提供了一个初始化列表版本,与我们的`operator=()`重载相同。 还需要注意的是,我们已经在每个功能中添加了`constexpr`。这是因为我们的定制容器中的大多数功能只不过是将一个调用从定制容器转发到`std::vector`,在某些情况下,还会调用`std::sort()`。`constexpr`的添加告诉编译器将代码视为编译时表达式,使其能够在启用优化时优化掉额外的函数调用(如果可能),从而确保我们的自定义包装器具有尽可能小的开销。 过去,这种类型的优化是使用`inline`关键字进行的。在 C++ 11 中添加的`constexpr`不仅能够为编译器提供`inline`提示,还能告诉编译器这个函数可以在编译时使用,而不是在运行时使用(这意味着编译器可以在编译代码时执行这个函数,以执行定制的编译时逻辑)。然而,在我们这里的例子中,运行时使用`std::vector`是不可能的,因为需要分配。因此,`constexpr`的使用只是为了优化,在大多数编译器上,`inline`关键字会提供类似的好处。 还有很多`std::vector`也支持的附加功能,比如`get_allocator()`、`empty()`、`size()`、`max_size()`,都只是直接转发。让我们把重点放在我们的定制容器中到目前为止缺少的访问器上: ```cpp constexpr const_reference at(size_type pos) const { return m_v.at(pos); } ``` 我们提供的第一个直接访问`std::vector`的功能是`at()`功能。与我们的大多数成员职能一样,这是一个直接的前进。然而,与`std::vector`不同,我们没有计划增加`std::vector`提供的`operator[]()`过载。`at()`函数和`operator[]()`重载的区别在于`operator[]()`不检查以确保提供的索引在边界内(也就是说,它不访问`std::vector`边界外的元素)。 `operator[]()`重载的功能类似于标准的 C 数组。这个操作符(被称为下标操作符)的问题是,缺少边界检查为可靠性和安全性缺陷进入您的程序打开了大门。因此,C++ 核心准则不鼓励使用下标运算符或任何其他形式的指针算法(任何试图通过使用指针计算数据位置而不进行显式边界检查的方法)。 为了防止使用`operator[]()`过载,我们不包括它。 像`std::vector`一样,我们也可以添加`front()`和`back()`访问器,如下所示: ```cpp constexpr const_reference front() const { return m_v.front(); } constexpr const_reference back() const { return m_v.back(); } ``` 前面的附加访问器为获取我们的`std::vector`中的第一个和最后一个元素提供了支持。与`at()`功能一样,我们只支持使用`std::vector`已经提供的这些功能的`const_reference`版本。 现在我们来看代码片段`data()`函数: ```cpp constexpr const T* data() const noexcept { return m_v.data(); } ``` `data()`功能也是如此。我们只能支持这些成员函数的`const`版本,因为提供这些函数的非常量版本将为用户提供对`std::vector`的直接访问,允许他们插入无序数据,而容器没有能力根据需要重新排序。 现在让我们关注比较运算符。我们首先将比较运算符的原型定义为容器的朋友。这是需要的,因为比较运算符通常实现为非成员函数,因此需要对容器的私有访问来比较它们包含的`std::vector`实例。 例如,考虑以下代码片段: ```cpp template friend constexpr bool operator==(const container &lhs, const container &rhs); template friend constexpr bool operator!=(const container &lhs, const container &rhs); template friend constexpr bool operator<(const container &lhs, const container &rhs); template friend constexpr bool operator<=(const container &lhs, const container &rhs); template friend constexpr bool operator>(const container &lhs, const container &rhs); template friend constexpr bool operator>=(const container &lhs, const container &rhs); ``` 最后,我们按如下方式实现比较运算符: ```cpp template bool constexpr operator==(const container &lhs, const container &rhs) { return lhs.m_v == rhs.m_v; } template bool constexpr operator!=(const container &lhs, const container &rhs) { return lhs.m_v != rhs.m_v; } ``` 和成员函数一样,我们只需要将调用转发到`std::vector`,因为不需要实现自定义逻辑。这同样适用于其余的比较运算符。 例如,我们可以如下实现`>`、`<`、`>=`和`<=`比较运算符: ```cpp template bool constexpr operator<(const container &lhs, const container &rhs) { return lhs.m_v < rhs.m_v; } template bool constexpr operator<=(const container &lhs, const container &rhs) { return lhs.m_v <= rhs.m_v; } template bool constexpr operator>(const container &lhs, const container &rhs) { return lhs.m_v > rhs.m_v; } template bool constexpr operator>=(const container &lhs, const container &rhs) { return lhs.m_v >= rhs.m_v; } ``` 就是这样!这就是如何通过利用现有的容器来实现自己的容器。 正如我们所看到的,在大多数情况下,没有必要从头实现容器,除非您需要的容器不能使用 C++ 标准模板库已经提供的容器之一来实现。 使用这种方法,不仅可以创建自己的容器,而且更重要的是,可以将代码中重复的功能封装到一个可以独立测试和验证的容器中。这不仅提高了应用的可靠性,还使它们更容易阅读和维护。 在下一章中,我们将探讨如何在 C++ 中使用智能指针。 ================================================ FILE: docs/adv-cpp-prog-cb/09.md ================================================ # 九、探索类型擦除 在本章中,您将了解什么是类型擦除(也称为类型擦除),以及如何在您自己的应用中使用它。本章很重要,因为类型擦除提供了处理不同类型对象的能力,而不需要对象共享一个公共基类。 本章首先简单解释了类型擦除,解释了类型擦除在 C 语言中是如何工作的,以及如何使用继承在 C++ 中执行类型擦除。下一个方法将提供一种使用 C++ 模板进行类型擦除的不同方法,它将教您如何使用 C++ 概念来定义类型的规范,而不是类型本身。 接下来,我们将介绍经典的 C++ 类型擦除模式。这个食谱将教你擦除类型信息的技巧,提供创建类型安全的通用代码的能力。最后,我们将以一个使用类型擦除来实现委托模式的综合示例来结束,这是一种提供包装任何类型的可调用对象的能力的模式,被像 ObjC 这样的语言大量使用。 本章中的配方如下: * 如何删除具有继承性的类型 * 使用 C++ 模板编写泛型函数 * 学习 C++ 类型的橡皮擦模式 * 实现委托模式 # 技术要求 要编译和运行本章中的示例,您必须拥有运行 Ubuntu 18.04 的计算机的管理权限,并且具有功能性互联网连接。在运行这些示例之前,您必须安装以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 如果这安装在 Ubuntu 18.04 以外的任何操作系统上,则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。 本章的代码文件可以在[https://github . com/packt publishing/Advanced-CPP-cook book/tree/master/chapter 09](https://github.com/PacktPublishing/Advanced-CPP-CookBook/tree/master/chapter09)找到。 # 如何删除具有继承性的类型 在这个食谱中,我们将学习如何使用继承来删除类型。当讨论类型擦除时,通常不考虑继承,但在现实中,它是 C++ 中使用的最常见的类型擦除形式。这个方法很重要,因为它将讨论什么是类型擦除,以及为什么它在日常应用中如此有用,而不仅仅是删除类型信息——这是 c 语言中常见的做法。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 让我们按照以下步骤来尝试这个食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter09 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe01_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe01_example01 1 0 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能,以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 类型擦除(或类型擦除)只是移除、隐藏或减少关于对象、功能等的类型信息的行为。在 C 语言中,类型擦除一直被使用。看看这个例子: ```cpp int array[10]; memset(array, 0, sizeof(array)); ``` 在前面的例子中,我们创建了一个`10`元素的数组,然后我们使用`memset()`函数将数组清零。C 语言中的`memset()`函数看起来是这样的: ```cpp void *memset(void *ptr, int value, size_t num) { size_t i; for (i = 0; i < num; i++) { ((char *)ptr)[i] = value; } return ptr; } ``` 如前面的代码片段所示,`memset()`函数取的第一个参数是`void*`。然而,前面例子中的数组是整数数组。`memset()`函数实际上并不关心您提供了什么类型,只要您提供了一个指向该类型的指针和一个以字节为单位表示该类型总大小的大小。`memset()`函数然后将提供的指针类型转换为代表一个字节的类型(在 C 语言中,这通常是`char`或无符号的`char`,然后逐字节设置该类型的值。 在 C 中使用`void*`是类型擦除的一种形式。在 C++ 中,这种类型(双关)的擦除通常是不鼓励的,因为获取类型信息的唯一方法是使用`dynamic_cast()`,这很慢(它需要运行时类型信息查找)。虽然在 C++ 中有很多方法可以执行类型擦除而不需要`void *`,但是让我们把重点放在继承上。 在大多数文献中,继承通常不被描述为类型擦除,但它可能是最广泛使用的类型擦除形式。为了更好地探索这是如何工作的,让我们看一个常见的例子。假设我们正在创建一个有多个超级英雄可供用户选择的游戏。每个超级英雄在某个时刻都要攻击坏人,但是超级英雄如何攻击坏人,每个英雄都不一样。 例如,考虑以下代码片段: ```cpp class spiderman { public: bool attack(int x, int) const { return x == 0 ? true : false; } }; ``` 如前面的代码片段所示,我们的第一个英雄不在乎坏人是在地上还是在空中(也就是说,无论坏人的垂直距离如何,英雄都会成功击中坏人),但是如果他们不在特定的水平位置,就会错过坏人。同样,我们也可能有另一个英雄如下: ```cpp class captain_america { public: bool attack(int, int y) const { return y == 0 ? true : false; } }; ``` 第二个英雄与我们的第一个完全相反。这个英雄可以在地面上的任何地方成功击中坏人,但是如果坏人在地面上的任何地方,这个英雄就会错过(英雄可能够不到他们)。 在下面的例子中,两个超级英雄同时在和坏人战斗: ```cpp for (const auto &h : heroes) { std::cout << h->attack(0, 42) << '\n'; } ``` 虽然我们可以在战斗中一次给每个超级英雄打一个电话,但如果我们能在战斗中循环每个英雄,看看哪个英雄击中了坏人,哪个英雄错过了坏人,那就方便多了。 在前面的例子中,我们有一个假设的英雄数组,我们循环遍历,检查哪个英雄命中,哪个英雄没有命中。在这个例子中,我们不关心英雄的类型(也就是说,我们不关心英雄具体是我们的第一个英雄还是第二个英雄),我们只关心每个英雄实际上是一个英雄(而不是一个无生命的物体),以及英雄有能力攻击坏人。换句话说,我们需要一种方法来删除每个超级英雄的类型,这样我们就可以将两个英雄放在一个数组中(除非每个英雄都是相同的,否则这是不可能的)。 正如您可能已经猜到的,在 C++ 中实现这一点最常见的方法是使用继承(但是正如我们将在本章后面展示的,这不是唯一的方法)。首先,我们必须首先定义一个名为`hero`的基类,每个英雄都将继承这个基类,如下所示: ```cpp class hero { public: virtual ~hero() = default; virtual bool attack(int, int) const = 0; }; ``` 在我们的例子中,每个英雄之间唯一的共同功能是他们都可以攻击坏人,`attack()`功能对所有英雄都是一样的。因此,我们创建了一个纯虚拟基类,其中有一个名为`attack()`的纯虚拟函数,每个英雄都必须实现它。还需要注意的是,一个类要成为纯虚函数,所有成员函数都必须设置为`0`,该类的析构函数必须显式标记为`virtual`。 现在我们已经定义了什么是英雄,我们可以修改我们的英雄来继承这个纯虚拟基类,如下所示: ```cpp class spiderman : public hero { public: bool attack(int x, int) const override { return x == 0 ? true : false; } }; class captain_america : public hero { public: bool attack(int, int y) const override { return y == 0 ? true : false; } }; ``` 如图所示,两个英雄都继承了英雄的纯虚拟定义,并根据需要覆盖了`attack()`功能。有了这个修改,我们现在可以创建我们的英雄列表如下: ```cpp int main(void) { std::array, 2> heros { std::make_unique(), std::make_unique() }; for (const auto &h : heros) { std::cout << h->attack(0, 42) << '\n'; } return 0; } ``` 从前面的代码中,我们观察到以下情况: * 我们创建一个`hero`指针数组(使用`std::unique_ptr`来存储英雄的寿命,这个话题将在下一章讨论)。 * 然后这个数组被初始化为包含两个英雄(每个英雄一个)。 * 最后,我们循环每个英雄,看看英雄是成功攻击坏人还是失手。 * 当调用`hero::attack()`函数时,调用会根据需要通过使用继承自动路由到正确的`spiderman::attack()`和`captain_america::attack()`函数。 该阵列正在以类型安全的方式擦除每个英雄的类型信息,以将每个英雄放入一个容器中。 # 使用 C++ 模板编写泛型函数 在这个食谱中,我们将学习如何利用 C++ 模板擦除(或忽略)类型信息。您将学习如何使用 C++ 模板来实现 C++ 概念,以及如何在 C++ 标准库中使用这种类型的擦除。这个方法很重要,因为它将教会你如何更好地将你的 API 设计成不依赖于特定类型的规范(或者,换句话说,如何编写通用代码)。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 让我们按照以下步骤来尝试这个食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter09 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe02_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe02_example01 hero won fight hero lost the fight :( ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... C++ 最古老、使用最广泛的特性之一是 C++ 模板。像继承一样,C++ 模板通常不被描述为类型擦除的一种形式,但它们确实是。类型擦除只不过是删除或者在这种情况下忽略类型信息的行为。 然而,与 C 语言不同的是,C++ 中的类型擦除通常试图避免移除类型信息,而倾向于在保留类型安全性的同时处理类型的严格定义。实现这一点的一种方法是通过使用 C++ 模板。为了更好地解释这一点,让我们从一个简单的 C++ 模板示例开始: ```cpp template T pow2(T t) { return t * t; } ``` 在前面的例子中,我们创建了一个简单的函数,可以计算任何给定输入的 2 的幂。例如,我们可以这样调用这个函数: ```cpp std::cout << pow2(42U) << '\n' std::cout << pow2(-1) << '\n' ``` 当编译器看到使用`pow2()`函数时,它会自动为您生成以下代码(幕后): ```cpp unsigned pow2(unsigned t) { return t * t; } int pow2(int t) { return t * t; } ``` 如前面的代码片段所示,编译器创建了两个版本的`pow2()`函数:一个版本接受无符号值并返回无符号值,另一个版本接受整数并返回整数。编译器创建这两个版本是因为第一次使用`pow2()`函数时,我们为其提供了无符号值,而第二次使用`pow2()`函数时,我们为其提供了`int`。 然而,就我们的代码而言,我们实际上并不关心函数提供的是什么类型,只要提供的类型能够成功执行`operator*()`。换句话说,`pow2()`函数的用户和`pow2()`函数的作者都在安全地忽略(或擦除)从概念角度传递给函数和从函数返回的类型信息。然而,编译器非常了解所提供的类型,并且必须根据需要安全地处理每种类型。 这种类型擦除的形式按照 API 的规范执行擦除,在 C++ 中,这种规范被称为概念。与大多数规定输入和输出类型的应用编程接口不同(例如,`sleep()`函数接受一个无符号整数,并且只接受一个无符号整数),一个概念特别忽略了类型,而是定义给定类型必须提供什么属性。 例如,前面的`pow2()`功能有以下要求: * 提供的类型必须是整数类型或提供`operator *()`。 * 提供的类型必须是可复制构造的或可移动构造的。 如前面的代码片段所示,`pow2()`函数并不关心给它什么类型,只要提供的类型满足某些最低要求。让我们研究一个更复杂的例子,演示如何将 C++ 模板用作类型擦除的一种形式。假设我们有两个不同的英雄在和坏人战斗,每个英雄都提供了攻击坏人的能力,如下代码所示: ```cpp class spiderman { public: bool attack(int x, int) const { return x == 0 ? true : false; } }; class captain_america { public: bool attack(int, int y) const { return y == 0 ? true : false; } }; ``` 如前面的代码片段所示,每个英雄都提供了攻击坏人的能力,但是除了碰巧两个英雄都提供了具有相同函数签名的`attack()`函数之外,两个英雄都没有任何共同点。我们也没有能力给每个英雄增加继承(可能我们的设计无法处理继承增加的额外`vTable`开销,也可能英雄定义是提供给我们的)。 现在假设我们有一个复杂的函数,必须为每个英雄调用`attack()`函数。我们可以为每个英雄编写相同的逻辑(也就是手动复制逻辑),或者我们可以编写一个 C++ 模板函数来为我们处理这个问题,如下所示: ```cpp template auto attack(const T &t, int x, int y) { if (t.attack(x, y)) { std::cout << "hero won fight\n"; } else { std::cout << "hero lost the fight :(\n"; } } ``` 如前面的代码片段所示,我们可以利用 C++ 模板的类型擦除属性将攻击逻辑封装到单个模板函数中。前面的代码不关心它提供的是什么类型,只要该类型提供了一个`attack()`函数,该函数接受两个整数类型并返回一个整数类型(最好是`bool`,但任何整数都可以)。换句话说,只要所提供的类型符合一个约定的概念,这个模板函数就可以工作,为编译器提供了一种为我们处理特定类型逻辑的方法。 我们可以如下调用前面的函数: ```cpp int main(void) { attack(spiderman{}, 0, 42); attack(captain_america{}, 0, 42); return 0; } ``` 这将产生以下输出: ![](img/70426f57-68a7-48bf-ac42-6ee95388297b.png) 虽然这个例子展示了 C++ 模板如何被用作类型擦除的一种形式(至少对于创建概念的规范来说),但是当讨论类型擦除时,有一种特定的模式被称为类型擦除模式或者仅仅是类型擦除。在下一个食谱中,我们将探索如何利用我们在前两个食谱中所学的知识来擦除类型信息,同时仍然支持容器等简单的东西。 # 还有更多... 在本食谱中,我们学习了如何使用概念来忽略(或擦除)特定于类型的知识,从而要求一个类型实现最少的一组特征。这些特性可以使用 SFINAE 来实现,这是我们在[第 4 章](04.html)、*使用通用编程模板*中详细讨论的主题。 # 请参见 在[第 13 章](13.html)、*奖励–使用 C++ 20 特性*中,我们还将讨论如何使用添加到 C++ 20 中的新特性来执行一个概念。 # 学习 C++ 类型的橡皮擦模式 在这个食谱中,我们将了解 C++ 中的类型擦除模式是什么,以及我们如何利用它来一般性地擦除类型信息,而不牺牲类型安全性或要求我们的类型继承纯虚拟基类。这个方法很重要,因为类型擦除模式在 C++ 标准库中被大量使用,它提供了一种简单的方法来封装没有任何共同点的数据类型,除了提供一组相似的 API,同时仍然支持容器之类的东西。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 让我们按照以下步骤来尝试这个食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter09 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe03_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe03_example01 1 0 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 当我们通常想到 C++ 类型擦除时,这就是我们想到的例子。当我们必须利用一组对象时,类型擦除模式是需要的,就像它们是相关的一样,它们可能共享也可能不共享一个公共基类(也就是说,它们要么不使用继承,要么如果它们使用继承,它们可能不从同一组类继承)。 例如,假设我们有以下类: ```cpp class spiderman { public: bool attack(int x, int) const { return x == 0 ? true : false; } }; class captain_america { public: bool attack(int, int y) const { return y == 0 ? true : false; } }; ``` 如前面的代码片段所示,每个类定义了不同类型的英雄。我们想做这样的事情: ```cpp for (const auto &h : heros) { // something } ``` 问题是,每个类都不是从相似的基类继承而来的,所以我们不能只创建每个类的一个实例并将它们添加到`std::array`中,因为编译器会抱怨类不一样。我们可以在`std::array`中存储每个类的原始`void *`指针,但是当需要使用`void *`时,我们必须`dynamic_cast()`回到每个类型来做任何有用的事情,如下所示: ```cpp std::array heros { new spiderman, new captain_america }; for (const auto &h : heros) { if (ptr = dynamic_cast(ptr)) { // something } if (ptr = dynamic_cast(ptr)) { // something } } ``` `void *`的使用是类型擦除的一种形式,但这远远不理想,因为`dynamic_cast()`的使用很慢,我们添加的每一种新类型只会增加`if`语句的数量,并且这种实现与 C++ 核心指南相去甚远。 然而,还有另一种方法可以解决这个问题。假设我们希望运行`attack()`函数,这个函数在每个英雄类之间恰好是相同的(也就是说,每个英雄类至少遵循一个共享的概念)。如果每个类都使用了下面的基类,我们可以只使用继承,如下所示: ```cpp class base { public: virtual ~base() = default; virtual bool attack(int, int) const = 0; }; ``` 问题是,我们的英雄类不是从这个基类继承的。因此,让我们创建一个包装类,如下所示: ```cpp template class wrapper : public base { T m_t; public: bool attack(int x, int y) const override { return m_t.attack(x, y); } }; ``` 如前面的代码片段所示,我们创建了一个继承自基类的模板包装类。这个包装器将一个实例存储到给定的任何类型,然后覆盖在纯虚拟基类中定义的`attack()`函数,该函数将对它的调用转发到包装器正在存储的实例。 现在,我们可以按如下方式创建阵列: ```cpp std::array, 2> heros { std::make_unique>(), std::make_unique>() }; ``` `std::array`将`std::unique_ptr`存储到我们的基类中,然后我们用我们需要的每种类型创建我们的包装类(它继承了基类),以存储在数组中。编译器为我们需要存储在数组中的每种类型创建一个包装器版本,由于包装器继承了基类,无论我们给包装器什么类型,数组总是可以根据需要存储结果包装器。 现在,从这个数组中,我们可以执行以下操作: ```cpp for (const auto &h : heros) { std::cout << h->attack(0, 42) << '\n'; } ``` 这就是:C++ 中的类型擦除。这种模式利用 C++ 模板赋予对象相同的继承属性,即使对象本身不直接使用继承。 # 用类型擦除实现委托 在这个食谱中,我们将学习如何实现委托模式,这是一个已经存在多年的模式(并且被其他一些语言大量使用,比如 ObjC)。这个方法很重要,因为它将教会你什么是委托,以及如何在你自己的应用中利用这个模式来提供更好的可扩展性,而不需要你的 API 使用继承。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 让我们按照以下步骤来尝试这个食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter09 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe04_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe04_example01 1 0 > ./recipe04_example02 1 0 > ./recipe04_example03 1 0 > ./recipe04_example04 0 1 0 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 如果你读过一本关于 C++ 的书,你可能已经看过苹果和橘子的例子,它演示了面向对象编程是如何工作的。想法如下: * 苹果是一种水果。 * 橘子是一种水果。 * 苹果不是橘子,但两者都是水果。 这个例子旨在教你如何使用继承将代码组织成逻辑对象。苹果和橙子共享的逻辑被写入名为`fruit`的对象,而苹果或橙子特有的逻辑被写入从基础`fruit`对象继承的`apple`或`orange`对象。 然而,这个例子也展示了如何扩展水果的功能。通过子类化一个水果,我可以创建一个苹果,它能做的比`fruit`基类更多。这种扩展类功能的想法在 C++ 中很常见,我们经常想到用继承来实现它。在这个食谱中,我们将探索如何做到这一点,而不需要苹果或橙子通过一种叫做委托的东西来利用继承。 假设你正在创建一个游戏,你希望实现一个英雄和坏人战斗的战场。在你的代码中的某个时候,战斗中的每个英雄都需要攻击坏人。问题是英雄在战斗中来来去去,因为他们需要时间来恢复,所以你真的需要维护一个能够攻击坏人的英雄列表,你只需要遍历这个动态变化的英雄列表,看看他们的攻击是否成功。 每个英雄可以存储一个英雄列表,这些英雄组成一个公共基类的子类,然后运行一个每个英雄覆盖的`attack()`函数,但是这需要使用继承,这可能是不希望的。我们也可以使用类型擦除模式包装每个英雄,然后存储指向我们包装器基类的指针,但是这将是我们的`attack()`函数所特有的,我们相信还会有其他需要这些类型扩展的情况。 输入委托模式,它是类型擦除模式的扩展。使用委托模式,我们可以编写如下代码: ```cpp int main(void) { spiderman s; captain_america c; std::array, 3> heros { delegate(attack), delegate(&s, &spiderman::attack), delegate(&c, &captain_america::attack) }; for (auto &h : heros) { std::cout << h(0, 42) << '\n'; } return 0; } ``` 如前面的代码片段所示,我们已经定义了两个不同类的实例,它们并不相同,然后我们创建了一个存储三个委托的数组。委托的模板参数采用`bool(int, int)`的函数签名,而委托本身似乎是由一个函数指针以及我们之前创建的类实例中的两个成员函数指针创建的。然后,我们能够遍历每个委托并调用它们,有效地独立调用函数指针和每个成员函数指针。 委托模式提供了将不同的可调用对象封装成一个具有公共类型的单个对象的能力,只要这些对象共享相同的函数签名,就能够调用这些可调用对象。更重要的是,委托可以封装函数指针和成员函数指针,为应用编程接口的用户提供在需要时存储私有状态的能力。 为了解释这是如何工作的,我们将从简单开始,然后以我们的例子为基础,直到最终实现。让我们从一个基类开始,如下所示: ```cpp template< typename RET, typename... ARGS > class base { public: virtual ~base() = default; virtual RET func(ARGS... args) = 0; }; ``` 如前面的代码片段所示,我们已经创建了一个纯虚拟基类的模板。模板参数是`RET`(定义返回值)和`ARGS...`(定义变量列表的参数)。然后我们创建了一个名为`func()`的函数,它接受我们的参数列表并返回模板返回类型。 接下来,让我们使用类型擦除模式定义一个从基类继承的包装器(如果您还没有阅读前面的方法,请现在阅读): ```cpp template< typename T, typename RET, typename... ARGS > class wrapper : public base { T m_t{}; RET (T::*m_func)(ARGS...); public: wrapper(RET (T::*func)(ARGS...)) : m_func{func} { } RET func(ARGS... args) override { return std::invoke(m_func, &m_t, args...); } }; ``` 就像类型橡皮擦模式一样,我们有一个包装器类,它存储我们类型的一个实例,然后提供一个包装器可以调用的函数。不同之处在于,可以调用的函数不是静态定义的,而是由提供的模板参数定义的。此外,我们还存储了一个具有相同函数签名的函数指针,它由包装器的构造函数初始化,并使用`std::invoke`在`func()`函数中调用。 与典型的类型擦除示例相比,这个额外的逻辑提供了定义我们希望从存储在包装器中的对象调用的任何函数签名的能力,而不是提前定义它(这意味着我们希望调用的函数可以在运行时而不是编译时确定)。 然后,我们可以如下创建委托类: ```cpp template< typename RET, typename... ARGS > class delegate { std::unique_ptr> m_wrapper; public: template delegate(RET (T::*func)(ARGS...)) : m_wrapper{ std::make_unique>(func) } { } RET operator()(ARGS... args) { return m_wrapper->func(args...); } }; ``` 与类型擦除模式一样,我们存储一个指向包装器的指针,它是从委托的构造函数创建的。这里需要认识的重要细节是`T`类型没有在委托本身中定义。相反,`T`类型仅在用于创建包装器实例化的委托的构造过程中是已知的。这意味着委托的每个实例都是相同的,即使委托存储了包装不同类型的包装。这允许我们如下使用委托。 假设我们有两个不共享一个公共基础的英雄,但是提供了一个具有相同签名的`attack()`函数: ```cpp class spiderman { public: bool attack(int x, int) { return x == 0 ? true : false; } }; class captain_america { public: bool attack(int, int y) { return y == 0 ? true : false; } }; ``` 我们可以利用我们的委托类来存储英雄类的实例,并按照如下方式调用它们的攻击函数: ```cpp int main(void) { std::array, 2> heros { delegate(&spiderman::attack), delegate(&captain_america::attack) }; for (auto &h : heros) { std::cout << h(0, 42) << '\n'; } return 0; } ``` 这将产生以下输出: ![](img/36666375-3829-4923-ab93-fc4ef67966c3.png) 虽然我们已经在创建委托方面取得了显著的进展(至少它是有效的),但是这种早期实现存在一些问题: * 委托的签名是`bool, int, int`,这是误导性的,因为我们真的想要一个像`bool(int, int)`这样的函数签名,这样代码是自文档化的(委托的类型是单个函数签名,而不是三个不同的类型)。 * 该委托不能处理标记为`const`的功能。 * 我们必须在包装器中存储委托对象的一个实例,这防止我们为同一个对象创建多个函数的委托。 * 我们不支持非成员函数。 让我们逐一解决这些问题。 # 向我们的委托添加函数签名 虽然向我们的委托添加函数签名作为模板参数可以在不需要 C++ 17 的情况下完成,但是 C++ 17 中的用户定义类型推导使这个过程变得简单。下面的代码片段显示了这一点: ```cpp template< typename T, typename RET, typename... ARGS > delegate(RET(T::*)(ARGS...)) -> delegate; ``` 如前面的代码片段所示,用户定义的类型推断告诉编译器如何获取我们的委托构造函数,并将其转换为我们希望使用的模板签名。如果没有这个用户定义的类型推导指南,`delegate(RET(T::*)(ARGS...))`构造函数将导致委托被推导为`delegate`,这不是我们想要的。相反,我们希望编译器推导出`delegate`。我们的委托实现没有其他需要改变的地方。我们只需要告诉编译器如何执行类型推导。 # 给我们的代表增加持续的支持 我们的委托当前不能接受标记为`const`的成员函数,因为我们没有为委托提供能够这样做的包装。例如,我们的英雄的`attack()`功能目前是这样的: ```cpp class spiderman { public: bool attack(int x, int) { return x == 0 ? true : false; } }; ``` 然而,我们希望我们的英雄`attack()`函数如下所示,因为它们不修改任何私有成员变量: ```cpp class spiderman { public: bool attack(int x, int) const { return x == 0 ? true : false; } }; ``` 为了支持这一更改,我们必须创建一个支持这一更改的包装器,如下所示: ```cpp template< typename T, typename RET, typename... ARGS > class wrapper_const : public base { T m_t{}; RET (T::*m_func)(ARGS...) const; public: wrapper_const(RET (T::*func)(ARGS...) const) : m_func{func} { } RET func(ARGS... args) override { return std::invoke(m_func, &m_t, args...); } }; ``` 如前所示,这个包装器与我们之前的包装器相同,区别在于我们存储的函数签名有一个添加的`const`实例。为了让委托使用这个附加的包装,我们还必须提供一个附加的委托构造函数,如下所示: ```cpp template delegate(RET (T::*func)(ARGS...) const) : m_wrapper{ std::make_unique>(func) } { } ``` 这意味着我们还需要一个额外的用户定义类型推导指南,如下所示: ```cpp template< typename T, typename RET, typename... ARGS > delegate(RET(T::*)(ARGS...) const) -> delegate; ``` 通过这些修改,我们现在可以支持标记有`const`的成员函数。 # 为我们的代表增加一对多支持 目前,我们的包装器为每种类型存储一个实例。这种方法通常用于类型擦除,但是在我们的例子中,它阻止了为同一个对象创建多个委托的能力(也就是说,不支持一对多)。为了解决这个问题,我们将在包装器中存储一个指向对象的指针,而不是对象本身,如下所示: ```cpp template< typename T, typename RET, typename... ARGS > class wrapper : public base { const T *m_t{}; RET (T::*m_func)(ARGS...); public: wrapper(const T *t, RET (T::*func)(ARGS...)) : m_t{t}, m_func{func} { } RET func(ARGS... args) override { return std::invoke(m_func, m_t, args...); } }; ``` 如前所示,我们所做的唯一更改是存储一个指向我们正在包装的对象的指针,而不是对象本身,这也意味着我们需要在构造函数中初始化这个指针。要使用这个新的包装,我们必须修改我们的委托构造函数,如下所示: ```cpp template delegate(const T *t, RET (T::*func)(ARGS...)) : m_wrapper{ std::make_unique>(t, func) } { } ``` 这反过来意味着我们必须更新用户定义的类型扣减指南,如下所示: ```cpp template< typename T, typename RET, typename... ARGS > delegate(const T *, RET(T::*)(ARGS...)) -> delegate; ``` 通过这些修改,我们现在可以如下创建代理: ```cpp int main(void) { spiderman s; captain_america c; std::array, 2> heros { delegate(&s, &spiderman::attack), delegate(&c, &captain_america::attack) }; for (auto &h : heros) { std::cout << h(0, 42) << '\n'; } return 0; } ``` 如前所述,委托获取指向每个对象的指针,这意味着我们可以创建任意多的委托,包括在需要时创建指向其他成员函数指针的委托。 # 向我们的委托添加对非成员函数的支持 最后,我们需要修改委托来增加对非成员函数的支持。看看这个例子: ```cpp bool attack(int x, int y) { return x == 42 && y == 42 ? true : false; } ``` 为此,我们只需添加另一个包装器,如下所示: ```cpp template< typename RET, typename... ARGS > class fun_wrapper : public base { RET (*m_func)(ARGS...); public: fun_wrapper(RET (*func)(ARGS...)) : m_func{func} { } RET func(ARGS... args) override { return m_func(args...); } }; ``` 如前所示,与我们的原始包装器一样,我们存储了指向我们希望调用的函数的指针,但是在这种情况下,我们不需要存储指向对象的指针,因为没有对象(因为这是非成员函数包装器)。要使用这个新包装,我们必须添加另一个委托构造函数,如下所示: ```cpp delegate(RET (func)(ARGS...)) : m_wrapper{ std::make_unique>(func) } { } ``` 这意味着我们还必须提供另一个用户定义的类型推导指南,如下所示: ```cpp template< typename RET, typename... ARGS > delegate(RET(*)(ARGS...)) -> delegate; ``` 经过所有的修改,我们终于能够使用本食谱开头定义的委托: ```cpp int main(void) { spiderman s; captain_america c; std::array, 3> heros { delegate(attack), delegate(&s, &spiderman::attack), delegate(&c, &captain_america::attack) }; for (auto &h : heros) { std::cout << h(0, 42) << '\n'; } return 0; } ``` 执行此操作时,我们会得到以下输出: ![](img/59994462-e91d-48fa-bcaf-1538fa6e4e37.png) 该委托可以通过添加另一组包装器来进一步扩展以支持 lambda 函数,并且委托中的`std::unique_pointer`的需求可以被移除以支持放置新的,使用成员函数包装器大小的小缓冲区(或者,换句话说,移除动态内存分配),这有时被称为小大小优化。 ================================================ FILE: docs/adv-cpp-prog-cb/10.md ================================================ # 十、对动态分配的深入研究 在本章中,您将学习如何使用动态内存分配。本章很重要,因为并非所有变量都可以全局定义或在堆栈上定义(即从函数内部定义),因为应该尽可能避免使用全局内存,堆栈内存通常比堆内存(用于动态内存分配的内存)更有限。然而,堆内存的使用多年来导致了大量关于泄漏和悬空指针的错误。 本章不仅将教您这种动态内存分配是如何工作的,还将教您如何以符合 C++ 核心指南的方式从堆中正确分配内存。 从我们为什么使用智能指针以及它们之间的区别、转换和其他引用开始,我们将在这一章结束时简要解释一下在 Linux 下堆是如何工作的,以及为什么动态内存分配如此缓慢。 在本章中,我们将介绍以下食谱: * 比较标准::共享 _ptr 和标准::唯一 _ptr * 从唯一 ptr 转换为共享 ptr * 使用循环引用 * 使用智能指针进行类型转换 * 显微镜下的堆 # 技术要求 要编译和运行本章中的示例,您必须拥有运行 Ubuntu 18.04 的计算机的管理权限,并且具有功能性互联网连接。在运行这些示例之前,必须使用以下命令安装 Valgrind: ```cpp > sudo apt-get install build-essential git cmake valgrind ``` 如果这安装在 Ubuntu 18.04 以外的任何操作系统上,则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。 本章的代码文件可以在[https://github . com/packt publishing/Advanced-CPP-cook book/tree/master/chapter 10](https://github.com/PacktPublishing/Advanced-CPP-CookBook/tree/master/chapter10)找到。 # 比较标准::共享 _ptr 和标准::唯一 _ptr 在本食谱中,我们将了解为什么 C++ 核心指南不鼓励使用手动调用 new 和 delete,以及为什么他们建议使用`std::unique_ptr`和`std::shared_ptr`。我们还将了解 a `std::unique_ptr`和 a `std::shared_ptr`之间的区别,以及为什么 a `std::shared_ptr`应该只在某些场景中使用(也就是为什么`std::unique_ptr`可能是您应该在大多数场景中使用的智能指针类型)。这个方法很重要,因为它将教你如何在现代 C++ 中正确分配动态(堆)内存。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 按照以下步骤完成该配方: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter10 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe01_examples ``` 3. 编译完源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe01_example01 > ./recipe01_example02 free(): double free detected in tcache 2 Aborted (core dumped) > ./recipe01_example03 > ./recipe01_example04 > ./recipe01_example05 > ./recipe01_example06 count: 42 > ./recipe01_example07 count: 33320633 > ./recipe01_example08 count: 42 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 在 C++ 中,有三种不同的方法来声明变量: * **全局变量**:这些是全局可访问的变量。在 Linux 上,这些通常存在于可执行文件的`.data`、`.rodata`或`.bss`部分。 * **栈变量**:这些变量是您在函数内部定义的,驻留在应用的栈内存中,由编译器管理。 * **堆变量**:这些是使用`malloc()` / `free()`或`new()` / `delete()`创建的变量,使用由动态内存管理算法管理的堆内存(例如,`dlmalloc`、`jemalloc`、`tcmalloc`等)。 在本章中,我们将重点讨论后者,即堆风格的内存分配。您可能已经知道,在 C++ 中,内存是使用`new()`和`delete()`分配的,如下所示: ```cpp int main(void) { auto ptr = new int; *ptr = 42; } ``` 我们可以看到,分配了一个整数指针(即指向整数的指针),然后设置为`42`。我们在 C++ 中使用`new()`而不是`malloc()`,原因如下: * `malloc()`返回`void *`而不是我们关心的类型。这可能会导致分配不匹配的错误(也就是说,您打算分配一辆汽车,而不是分配一辆橙色汽车)。换句话说,`malloc()`不提供类型安全。 * `malloc()`需要一个尺寸参数。为了分配内存,我们需要知道为我们关心的类型分配多少字节。这可能会导致分配大小不匹配的错误(也就是说,您打算为一辆汽车分配足够的字节,但实际上您只为一辆橙色汽车分配了足够的字节)。 * `malloc()`出错时返回`NULL`,要求`NULL`对每次分配进行检查。 `new()`操作员解决所有这些问题: * `new()`返回`T*`。如上例所示,这甚至允许使用`auto`,防止冗余,因为 C++ 的类型系统有足够的信息来正确分配和跟踪所需的类型。 * `new()`不接受大小论证。相反,您告诉它您想要分配什么类型,它已经隐式地拥有了关于该类型的大小信息。再一次,通过简单地陈述你想要分配什么,你得到了你想要分配的,包括适当的指针和大小。 * `new()`如果分配失败,抛出异常。这防止了对`NULL`检查的需要。如果执行了下一行代码,就可以保证分配成功(假设没有禁用异常)。 然而`new()`操作者还有一个问题;`new()`不跟踪所有权。像`malloc()`一样,`new()`操作符返回一个指针,这个指针可以在函数之间传递,而不知道谁真正拥有这个指针,这意味着它应该在不再需要的时候删除这个指针。 这种所有权的概念是 C++ 核心指南(除了内存跨度之外)的一个关键组成部分,该指南试图解决 C++ 中导致不稳定、可靠性和安全性错误的常见错误。让我们看一个例子: ```cpp int main(void) { auto p = new int; delete p; delete p; } ``` 在前面的例子中,我们分配了一个整数指针,然后删除该指针两次。在前面的例子中,我们从未在退出程序之前删除整数指针。现在,考虑以下代码块: ```cpp int main(void) { auto p = new int; delete p; *p = 42; } ``` 在前面的例子中,我们分配一个整数指针,删除它,然后使用它。虽然这些例子看起来很简单,很容易避免,但是在大型复杂的项目中,这些类型的错误经常发生,以至于 C++ 社区开发了静态和动态分析工具来自动为我们识别这些类型的错误(尽管它们并不完美),以及 C++ 核心指南本身,试图从一开始就防止这些类型的错误。 在 C++ 11 中,标准委员会引入了`std::unique_ptr`来解决与`new()`和`delete()`的所有权问题。以下是它的工作原理: ```cpp #include int main(void) { auto ptr = std::make_unique(); *ptr = 42; } ``` 在前面的例子中,我们使用`std::make_unique()`函数分配了一个整数指针。这个函数创建一个`std::unique_ptr`,并给它一个使用`new()`分配的指针。这里,得到的指针(大部分)看起来和行为都像一个常规指针,除了当`std::unique_ptr`失去作用域时指针会被自动删除。也就是说,`std::unique_ptr`拥有使用`std::make_unique()`分配的指针,并对指针本身的生存期负责。在本例中,我们不需要手动运行`delete()`,因为`delete()`是在`main()`功能完成时为我们运行的(也就是当`std::unique_ptr`失去作用域时)。 使用这个管理所有权的简单技巧,可以避免前面代码中显示的所有错误(大部分,我们将在后面讨论)。虽然下面的代码不符合 c++ Core guide(因为不建议使用下标运算符),但是您也可以使用`std::unique_ptr`分配数组,如下所示: ```cpp #include #include int main(void) { auto ptr = std::make_unique(100); ptr[0] = 42; } ``` 如前面的代码所示,我们分配一个大小为`100`的 C 风格数组,然后设置数组中的第一个元素。一般来说,你唯一需要的指针类型是`std::unique_ptr`。然而,仍然会出现一些问题: * 未正确跟踪指针的生存期,例如,在函数中分配`std::unique_ptr`并返回结果指针。一旦函数返回,`std::unique_ptr`将失去作用域,从而删除刚刚返回的指针。`std::unique_ptr` *不*实行自动垃圾收集。您仍然需要了解指针的生存期以及它如何影响您的代码。 * 永远不为`std::unique_ptr`提供失去作用域的机会,仍然有可能泄漏内存(尽管难度要大得多);例如,将`std::unique_ptr`添加到全局列表中,或者在用`new()`手动分配的类中分配`std::unique_ptr`,然后泄漏。`std::unique_ptr` *又一次没有*实现自动垃圾回收,仍然需要你保证`std::unique_ptr`在需要的时候失去作用。 * `std::unique_ptr`也没有能力支持共享所有权。虽然这是一个问题,但这种情况很少发生。在大多数情况下,您只需要`std::unique_ptr`就可以确保正确的所有权。 经常提出的一个问题是,*一旦分配了指针,我们如何安全地将这个指针传递给其他函数?*答案是,使用`get()`函数,将指针作为常规的 C 风格指针传递。`std::unique_ptr`定义的是所有权,而不是`NULL`的指针安全。`NULL`指针安全由带有`gsl::not_null`包装器和`expects()`宏的指南支持库提供。 如何使用这些取决于您的指针哲学: * 一些人认为任何以指针为参数的函数都应该检查`NULL`指针。这种方法的优点是可以快速识别并安全处理`NULL`指针,缺点是您在代码中引入了额外的分支逻辑,这会降低性能和可读性。 * 一些人认为应该检查以指针为参数的*公共*函数是否有`NULL`指针。这种方法的优点是提高了性能,因为并非所有函数都需要`NULL`指针检查。这种方法的缺点是公共接口仍然有额外的分支逻辑。 * 一些人认为函数应该简单地记录它的期望(称为契约)。这种方法的好处是`assert()`和`expects()`宏可以用来在调试模式下检查`NULL`指针以强制执行该约定,而在发布模式下,没有性能损失。这种方法的缺点是,在释放模式下,所有赌注都被取消。 您采取哪种方法将在很大程度上取决于您正在编写的应用的类型。如果你正在写下一个 Crush 游戏,你可能会更关心后一种方法,因为它表现最好。如果你正在编写一个自动驾驶飞机的应用,我们都希望你使用第一种方法。 为了演示如何使用`std::unique_ptr`传递指针,让我们看下面的例子: ```cpp std::atomic count; void inc(int *val) { count += *val; } ``` 假设您有一个作为线程执行的超关键函数,以整数指针作为参数,并将提供的整数添加到全局计数器中。这个线程的前一个实现是*下注*,祈祷最好的方法。该功能可以如下实现: ```cpp void inc(int *val) { if (val != nullptr) { count += *val; } else { std::terminate(); } } ``` 如果提供的指针是`NULL`指针,前面的函数调用`std::terminate()`(不是一个非常容错的方法)。正如我们所看到的,这种方法很难理解,因为这里有很多额外的逻辑。我们可以这样实现: ```cpp void inc(gsl::not_null val) { count += *val; } ``` 这与`NULL`指针检查做了同样的事情(取决于您如何定义`gsl::not_null`工作,因为这也可能引发异常)。您也可以如下实现: ```cpp void inc(int *val) { expects(val); count += *val; } ``` 前面的例子总是检查`NULL`指针,而前面的方法使用契约方法,允许在发布模式下取消检查。您也可以使用`assert()`(如果您没有使用 GSL...这开玩笑地说,当然,不应该是这种情况)。 还应该注意的是,C++ 标准委员会正致力于通过使用 C++ 契约来添加`expects()`逻辑作为语言的核心组件,这一特性不幸地从 C++ 20 中删除了,但有望在标准的未来版本中添加,因为我们可能能够如下编写前面的函数(并告诉编译器我们希望使用哪种方法,而不是必须手动编写它): ```cpp void inc(int *val) [[expects: val]] { count += *val; } ``` 我们可以如下使用这个函数: ```cpp int main(void) { auto ptr = std::make_unique(1); std::array threads; for (auto &thread : threads) { thread = std::thread{inc, ptr.get()}; } for (auto &thread : threads) { thread.join(); } std::cout << "count: " << count << '\n'; return 0; } ``` 从前面的代码示例中,我们可以观察到以下内容: * 我们使用`std::make_unique()`从堆中分配一个整数指针,返回`std::unique_ptr()`。 * 我们创建一个线程数组并执行每个线程,将新分配的指针传递给每个线程。 * 最后,我们等待所有线程完成并输出结果计数。由于`std::unique_ptr`的作用域是`main()`函数,我们必须确保线程在从`main()`函数返回之前完成。 前面的示例产生以下输出: ![](img/174f4cca-9a24-4400-8cc9-193f3b2d646b.png) 正如我们前面提到的,前面的例子将`std::unique_ptr`定义为`main()`函数的范围,这意味着我们必须确保线程在`main()`函数返回之前完成。这种情况并不总是如此。让我们看看下面的例子: ```cpp std::atomic count; void inc(int *val) { count += *val; } ``` 在这里,我们创建了一个函数,当给定一个整数指针时,该函数增加一个计数: ```cpp int main(void) { std::array threads; { auto ptr = std::make_unique(1); for (auto &thread : threads) { thread = std::thread{inc, ptr.get()}; } } for (auto &thread : threads) { thread.join(); } std::cout << "count: " << count << '\n'; return 0; } ``` 如前面的代码所示,`main()`函数也与我们前面的例子相同,只是`std::unique_ptr`是在自己的作用域中创建的,它是在线程需要完成之前释放的。这将产生以下输出: ![](img/cf6bd321-786d-4be2-9694-3287c2c6229f.png) 如前面的截图所示,当线程试图从已被删除的内存中读取时,结果输出是垃圾(也就是说,线程被赋予了一个悬空指针)。 虽然这是一个简单的例子,但这种类型的场景可能发生在更复杂的场景中,问题的根源是共享所有权。在这个例子中,每个线程都拥有指针。换句话说,没有一个线程试图获得指针的唯一所有权(包括分配和执行其他线程的主线程)。虽然这种类型的问题通常发生在没有主线程设计的多线程应用中,但这也可能发生在异步逻辑中,在异步逻辑中,指针被分配,然后被传递给生命周期和执行点未知的多个异步作业。 为了处理这些特定类型的问题,C++ 提供了`std::shared_ptr`。这是托管对象的包装。每次复制`std::shared_ptr`时,被管理对象都会增加一个内部计数器,用于跟踪指针(被管理对象存储的)有多少个所有者。每当`std::shared_ptr`失去作用域时,被管理对象减少内部计数器,并且一旦该计数达到`0`就删除指针。使用这种方法,`std::shared_ptr`能够支持一对多所有权模型,该模型可以处理我们之前定义的场景。 让我们看看下面的例子: ```cpp std::atomic count; void inc(std::shared_ptr val) { count += *val; } ``` 如前面的代码所示,我们有相同的递增计数器的线程函数,但不同的是它采用`std::shared_ptr`而不是常规的整数指针。现在,我们可以如下实现前面的示例: ```cpp int main(void) { std::array threads; { auto ptr = std::make_shared(1); for (auto &thread : threads) { thread = std::thread{inc, ptr}; } } for (auto &thread : threads) { thread.join(); } std::cout << "count: " << count << '\n'; return 0; } ``` 如前面的代码所示,指针是在其自己的作用域中创建的,该作用域在线程需要完成之前被移除。但是,与前面的示例不同,这段代码会产生以下结果: ![](img/510ee43e-1a06-46ff-8411-882af9ebb984.png) 前面的代码正确执行的原因是指针的所有权在所有线程之间共享,指针本身在所有线程完成之前不会被删除(即使作用域丢失)。 最后一个注意事项:当应该使用`std::unique_ptr`时,对所有指针类型使用`std::shared_ptr`可能很有诱惑力,因为它有很好的类型转换 API,并且在理论上确保函数有有效的指针。现实情况是,不管使用`std::shared_ptr`还是`std::unique_ptr`,一个函数都必须按照应用的需求执行其`NULL`检查,因为`std::shared_ptr`仍然可以被创建为`NULL`指针。 `std::shared_ptr`也增加了开销,因为它必须在内部存储所需的删除程序。它还需要为托管对象分配额外的堆。`std::shared_ptr`和`std::unique_ptr`都定义了指针所有权。它们不提供自动垃圾收集(也就是说,它们不自动处理指针生存期),也不保证某个指针不是`NULL`。`std::shared_ptr`应该只在多个事物必须拥有指针的生存期时使用,以确保应用的正确执行;否则,使用`std::unique_ptr`。 # 从标准::唯一 _ptr 转换为标准::共享 _ptr 在这个食谱中,我们将学习如何从`std::unique_ptr`转换成`std::shared_ptr`。这个配方很重要,因为当应用编程接口本身确实需要`std::shared_ptr`用于内部使用时,将应用编程接口定义为接受`std::unique_ptr`通常很方便。一个很好的例子是在创建图形用户界面应用编程接口时。您可能会将一个小部件传递给应用编程接口来存储和拥有,而不知道以后图形用户界面的实现是否需要添加线程,在这种情况下`std::shared_pointer`可能是一个更好的选择。该配方将为您提供将`std::unique_ptr`转换为`std::shared_ptr`的技能,如果需要的话,无需修改 API 本身。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 按照以下步骤完成该配方: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter10 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe02_examples ``` 3. 编译完源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe02_example01 count: 42 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... `std::shared_ptr`用于在多个事物必须拥有指针才能使应用正常执行时管理指针。但是,假设您提供了一个必须接受整数指针的 API,如下所示: ```cpp void execute_threads(int *ptr); ``` 前面的 API 建议调用这个函数的人拥有整数指针。也就是说,无论谁调用这个函数,都需要分配整数指针,并在函数完成后删除它。但是,如果我们打算让前面的应用编程接口拥有指针,那么我们真的应该这样编写这个应用编程接口: ```cpp void execute_threads(std::unique_ptr ptr); ``` 这个 API 说,*请给我分配一个整数指针,但是它一旦通过,我就拥有它,并且会保证在需要的时候删除它。*现在,假设这个函数将在一对多所有权场景中使用这个指针。你是做什么的?您可以按如下方式编写您的应用编程接口: ```cpp void execute_threads(std::shared_ptr ptr); ``` 然而,这将阻止您的应用编程接口在未来优化一对多关系(也就是说,如果您能够在未来删除这种关系,您仍然会被`std::shared_ptr`卡住,即使它是次优的,而不必修改应用编程接口的函数签名)。 为了解决这个问题,c++ API 提供了将`std::unique_ptr`转换为`std::shared_ptr`的能力,如下所示: ```cpp std::atomic count; void inc(std::shared_ptr val) { count += *val; } ``` 假设我们有一个内部函数,就目前而言,将一个整数指针作为`std::shared_ptr`,使用它的值来递增`count`,并将其作为一个线程来执行。然后,我们为它提供一个公共 API 来使用这个内部函数,如下所示: ```cpp void execute_threads(std::unique_ptr ptr) { std::array threads; auto shared = std::shared_ptr(std::move(ptr)); for (auto &thread : threads) { thread = std::thread{inc, shared}; } for (auto &thread : threads) { thread.join(); } } ``` 如前面的代码所示,我们的 API 声明了先前分配的整数指针的所有权。然后,它创建一系列线程,执行每个线程并等待每个线程完成。问题是,我们的内部函数需要一个`std::shared_ptr`(例如,可能这个内部函数在代码中的其他地方使用,那里有一对多的所有权场景,我们目前无法移除)。 为了防止需要用`std::shared_ptr`定义我们的公共 API,我们可以通过将`std::unique_ptr`移动到新的`std::shared_ptr`中,然后从那里调用我们的线程,将`std::unique_ptr`转换为`std::shared_ptr`。 `std::move()`是必需的,因为传递`std::unique_ptr`所有权的唯一方式是通过使用`std::move()`(因为在任何给定时间只有一个`std::unique_ptr`可以拥有指针)。 现在,我们可以如下执行这个公共 API: ```cpp int main(void) { execute_threads(std::make_unique(1)); std::cout << "count: " << count << '\n'; return 0; } ``` 这将产生以下输出: ![](img/3662afb6-a730-4b83-a259-0d9182ad87de.png) 将来,我们也许能够消除对`std::shared_ptr`的需求,并使用`get()`函数将`std::unique_ptr`传递给我们的内部函数,并且,当那个时候到来时,我们将不必修改公共 API。 # 使用循环引用 在这个食谱中,我们将学习如何使用循环引用。当我们使用多个`std::shared_ptr`时,循环引用发生,其中每个`std::shared_ptr`拥有对另一个的引用。这个方法很重要,因为当我们处理循环依赖对象时,这种类型的循环引用可能会发生(尽管这应该尽可能避免)。如果真的发生了,`std::shared_ptr`的共享特性会导致内存泄漏。本食谱将为您提供使用`std::weak_ptr`避免上述内存泄漏的技巧。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake valgrind ``` 完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 要使用循环引用,请执行以下步骤: 1. 从一个新的终端,运行以下程序来下载该配方的源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter10 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe03_examples ``` 3. 编译完源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > valgrind ./recipe03_example01 ... ==7960== HEAP SUMMARY: ==7960== in use at exit: 64 bytes in 2 blocks ==7960== total heap usage: 3 allocs, 1 frees, 72,768 bytes allocated ... > valgrind ./recipe03_example02 ... ==7966== HEAP SUMMARY: ==7966== in use at exit: 64 bytes in 2 blocks ==7966== total heap usage: 4 allocs, 2 frees, 73,792 bytes allocated ... > valgrind ./recipe03_example03 ... ==7972== HEAP SUMMARY: ==7972== in use at exit: 0 bytes in 0 blocks ==7972== total heap usage: 4 allocs, 4 frees, 73,792 bytes allocated ... > valgrind ./recipe03_example04 ... ==7978== HEAP SUMMARY: ==7978== in use at exit: 0 bytes in 0 blocks ==7978== total heap usage: 4 allocs, 4 frees, 73,792 bytes allocated ... ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 尽管应该避免循环引用,但是随着项目变得越来越复杂和庞大,循环引用很可能会出现。如果在这些循环引用发生时利用共享智能指针,可能会发生难以发现的内存泄漏。为了理解这是如何实现的,让我们看下面的例子: ```cpp class car; class engine; ``` 如前面的代码所示,我们从两个类原型开始。循环引用几乎总是以这种方式开始,因为一个类依赖于另一个类,反之亦然,需要使用类原型。 让我们定义一个`car`如下: ```cpp class car { friend void build_car(); std::shared_ptr m_engine; public: car() = default; }; ``` 如前面的代码所示,这是一个简单的类,它存储了一个指向`engine`的共享指针和一个名为`build_car()`的友元函数。现在,我们可以定义一个`engine`如下: ```cpp class engine { friend void build_car(); std::shared_ptr m_car; public: engine() = default; }; ``` 如前面的代码所示,一个`engine`类似于一个`car`,区别在于引擎存储了一个指向汽车的共享指针。不过,两者都有`build_car()`功能。两者都创建默认构造的共享指针,这意味着它们的共享指针在构造时是`NULL`指针。 `build_car()`功能用于完成每个对象的构建,如下所示: ```cpp void build_car() { auto c = std::make_shared(); auto e = std::make_shared(); c->m_engine = e; e->m_car = c; } ``` 如前面的代码所示,我们创建每个对象,然后设置汽车的引擎,反之亦然。由于汽车和发动机都在`build_car()`函数的范围内,我们预计一旦`build_car()`函数返回,这些指针将被删除。现在,我们可以如下执行这个`build_car()`功能: ```cpp int main(void) { build_car(); return 0; } ``` 这看起来像一个简单的程序,但它有一个很难发现的内存泄漏。为了演示这一点,让我们在`valgrind`中运行这个应用,这是一个能够检测内存泄漏的动态内存分析工具: ![](img/d039e626-58e3-4399-88f3-bb7b58a9d235.png) 如前面截图所示,`valgrind`表示内存泄露。如果我们用`--leak-check=full`运行`valgrind`,它会告诉我们内存泄漏是汽车和发动机共享指针。发生这种内存泄漏的原因是汽车拥有对引擎的共享引用。同样的引擎拥有对汽车本身的共享引用。 例如,考虑以下代码: ```cpp void build_car() { auto c = std::make_shared(); auto e = std::make_shared(); c->m_engine = e; e->m_car = c; std::cout << c.use_count() << '\n'; std::cout << e.use_count() << '\n'; } ``` 如前面的代码所示,我们添加了对`use_count()`的调用,该调用输出`std::shared_ptr`包含的所有者数量。如果执行此操作,我们将看到以下输出: ![](img/0fe8e3a7-4ec9-46f6-9e6f-14f91b815586.png) 我们能看到两个车主的原因是因为`build_car()`函数在这里保存了对一辆车和一台发动机的引用: ```cpp auto c = std::make_shared(); auto e = std::make_shared(); ``` 汽车第二次提到发动机是因为: ```cpp c->m_engine = e; ``` 发动机和汽车也是如此。当`build_car()`功能完成时,以下内容首先失去作用域: ```cpp auto e = std::make_shared(); ``` 然而,引擎并没有被删除,因为汽车仍然保存着对引擎的引用。然后,汽车失去了作用范围: ```cpp auto c = std::make_shared(); ``` 然而,汽车并没有被删除,因为引擎(还没有被删除)也保存着对汽车的引用。这导致`build_car()`返回时,汽车和引擎都没有被删除,因为两者仍然保持相互引用,没有办法告诉任何一个对象删除它们的引用。 这种类型的循环内存泄漏虽然在我们的示例中很容易识别,但在复杂的代码中却非常难识别,这是应该避免共享指针和循环依赖的许多原因之一(通常更好的设计可以消除对两者的需求)。如果无法避免,可以使用`std::weak_ptr`代替,如下所示: ```cpp class car { friend void build_car(); std::shared_ptr m_engine; public: car() = default; }; ``` 如前面的代码所示,我们仍然将我们的汽车定义为持有对引擎的共享引用。我们这样做是因为我们假设汽车的寿命更长(也就是说,在我们的模型中,你可以有一辆没有发动机的汽车,但你不能有一个没有汽车的发动机)。然而,发动机的定义如下: ```cpp class engine { friend void build_car(); std::weak_ptr m_car; public: engine() = default; }; ``` 如前面的代码所示,引擎现在存储了对汽车的弱引用。我们的`build_car()`功能定义如下: ```cpp void build_car() { auto c = std::make_shared(); auto e = std::make_shared(); c->m_engine = e; e->m_car = c; std::cout << c.use_count() << '\n'; std::cout << e.use_count() << '\n'; } ``` 如前代码所示,`build_car()`功能不变。现在的不同之处在于,当我们使用`valgrind`执行这个应用时,我们会看到以下输出: ![](img/0ac121f9-776c-48ff-a5e0-fdbe56d97fa4.png) 如上图截图所示,没有内存泄漏,汽车的`use_count()`为`1`,而发动机的`use_count()`与上例相比仍为`2`。在引擎类中,我们使用`std::weak_ptr`,它可以访问`std::shared_ptr`管理的托管对象,但是在创建时不会增加托管对象的内部计数。这为`std::weak_ptr`提供了查询`std::shared_ptr`是否有效的能力,而不必持有对指针本身的强引用。 内存泄漏被清除的原因是,当发动机失去作用域时,其使用次数从`2`减少到`1`。一旦汽车失去作用范围,只有`1`的使用计数,它被删除,这反过来减少发动机的使用计数到`0`,这导致发动机也被删除。 我们在引擎中使用`std::weak_ptr`而不是 C 风格指针的原因是`std::weak_ptr`为我们提供了查询托管对象的能力,以查看指针是否仍然有效。例如,假设我们需要检查汽车是否仍然存在,如下所示: ```cpp class engine { friend void build_car(); std::weak_ptr m_car; public: engine() = default; void test() { if (m_car.expired()) { std::cout << "car deleted\n"; } } }; ``` 使用`expired()`功能,我们可以在使用前测试看看车是否还存在,这是 C 型指针无法做到的。现在,我们可以将我们的`build_car()`函数编写如下: ```cpp void build_car() { auto e = std::make_shared(); { auto c = std::make_shared(); c->m_engine = e; e->m_car = c; } e->test(); } ``` 在前面的例子中,我们创建了一个引擎,然后创建了一个新的范围来创建我们的汽车。然后,我们创建循环引用并失去作用域。这导致汽车如预期的那样被删除。不同的是,我们的引擎还没有被删除,因为我们仍然拥有对它的引用。现在,我们可以运行我们的测试函数,当它与`valgrind`一起运行时,会产生以下输出: ![](img/37efcbf4-6ee5-4fef-b31a-150aa6c7b76f.png) 如前面的截图所示,没有内存泄漏。`std::weak_ptr`成功去除了循环引用引入的鸡和蛋问题。因此,`std::shared_ptr`能够按预期运行,以正确的顺序释放内存。一般来说,应该尽可能避免循环引用和依赖关系,但是,如果无法避免,可以使用`std::weak_ptr`,如本食谱所示,来防止内存泄漏。 # 使用智能指针进行类型转换 在本食谱中,我们将学习如何使用`std::unique_ptr`和`std::shared_ptr`进行打字。类型转换允许您将一种类型转换成另一种类型。这个方法很重要,因为它展示了当试图转换智能指针的类型时(例如,当使用虚拟继承进行向上转换或向下转换时)使用`std::unique_ptr`和`std::shared_ptr`处理类型转换的正确方法。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 要了解类型转换的工作原理,请执行以下步骤: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter10 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe04_examples ``` 3. 编译完源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe04_example01 downcast successful!! > ./recipe04_example02 downcast successful!! ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 使用智能指针进行类型转换并不像您预期的那样简单。 为了更好地解释这一点,让我们看一个简单的例子,说明如何使用`std::unique_ptr`从基类向子类进行类型转换: ```cpp class base { public: base() = default; virtual ~base() = default; }; ``` 让我们看看这是如何工作的: 1. 我们从一个虚拟基类开始,如前面的代码所示,然后我们将基类子类化如下: ```cpp class subclass : public base { public: subclass() = default; ~subclass() override = default; }; ``` 2. 接下来,我们在`main()`函数中创建一个`std::unique_ptr`,并将指针传递给一个`foo()`函数: ```cpp int main(void) { auto ptr = std::make_unique(); foo(ptr.get()); return 0; } ``` `std::unique_ptr`只是拥有指针的生存期。指针的任何使用都需要使用`get()`函数,该函数从该点开始将`std::unique_ptr`转换为正常的 C 型指针。这是`std::unique_ptr`的预期用途,因为它不是为了确保指针安全而设计的,而是为了确保谁拥有指针被很好地定义,最终决定何时删除指针。 3. 现在`foo()`功能可以定义如下: ```cpp void foo(base *b) { if (dynamic_cast(b)) { std::cout << "downcast successful!!\n"; } } ``` 如前面的代码所示,`foo()`函数可以将指针视为普通的 C 风格指针,使用`dynamic_cast()`从基指针向下转换回原始子类。 这种相同风格的类型转换是标准的 C++,不适用于`std::shared_ptr`。原因是因为需要类型转换版本的`std::shared_ptr`的代码可能还需要保存对指针的引用(也就是说,`std::shared_ptr`的副本以防止删除)。 也就是说,不可能从`base *b`到`std::shared_ptr`,因为`std::shared_ptr`没有指针的引用;相反,它保存对托管对象的引用,托管对象存储对实际指针的引用。由于`base *b`不存储托管对象,因此无法从中创建`std::shared_ptr`。 然而,C++ 确实提供了`static_cast()`、`reinterpret_cast()`、`const_cast()`和`dynamic_cast()`的`std::shared_ptr`版本来执行共享指针的类型转换,这在类型转换时保留了托管对象。让我们看一个例子: ```cpp class base { public: base() = default; virtual ~base() = default; }; class subclass : public base { public: subclass() = default; ~subclass() override = default; }; ``` 如前面的代码所示,我们从相同的基类和子类开始。区别出现在我们的`foo()`函数中: ```cpp void foo(std::shared_ptr b) { if (std::dynamic_pointer_cast(b)) { std::cout << "downcast successful!!\n"; } } ``` 不取`base *b`,取`std::shared_ptr`。现在,我们可以使用`std::dynamic_pointer_cast()`功能代替`dynamic_cast()`将`std::shared_ptr`降频至`std::shared_ptr`。`std::shared_ptr`类型转换功能为我们提供了类型转换的能力,同时根据需要保持对`std::shared_ptr`的访问。 产生的`main()`函数如下所示: ```cpp int main(void) { auto ptr = std::make_shared(); foo(ptr); return 0; } ``` 这将产生以下输出: ![](img/8b7d0aa2-1117-4b34-a220-0b1bba6777d6.png) 应该注意的是,我们不需要显式上转换,因为这可以自动完成(类似于常规指针)。我们只需要显式向下转换。 # 显微镜下的堆 在这个食谱中,我们将学习堆在 Linux 中是如何工作的。我们将深入研究当您使用`std::unique_ptr`时,Linux 实际上是如何提供堆内存的。 虽然这个方法是为那些拥有更高级功能的人准备的,但它很重要,因为它将教会你应用如何从堆中分配内存(也就是说,使用`new()` / `delete()`),这反过来将向你展示为什么堆分配永远不应该从时间关键的代码中完成,因为它们很慢。当堆分配可以安全执行时,当应用中应该避免堆分配时,即使我们检查的一些汇编代码很难遵循,这个方法也会教你所需的技能。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 要尝试本章的代码文件,请执行以下步骤: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter10 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe05_examples ``` 3. 编译完源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe05_example01 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 为了更好地理解代码必须执行到什么程度才能在堆上分配变量,我们将从下面的简单示例开始: ```cpp int main(void) { auto ptr = std::make_unique(); } ``` 如前例所示,我们使用`std::unique_ptr()`分配一个整数。我们使用`std::unique_ptr()`作为我们的起点,因为这是大多数 C++ 核心指南代码在堆上分配内存的方式。 `std::make_unique()`函数使用以下伪逻辑分配一个`std::unique_ptr`(这是一个简化的例子,因为它没有显示如何处理自定义删除程序): ```cpp namespace std { template auto make_unique(ARGS... args) { return std::unique_ptr(new T(std::forward(args)...)); } } ``` 如前面的代码所示,`std::make_unique()`函数创建了一个`std::unique_ptr`,并给它一个指针,该指针是用`new()`运算符分配的。一旦`std::unique_ptr`失去作用域,它将使用`delete()`删除指针。 当编译器看到新的运算符时,它会用对运算符`new(unsigned long)`的调用来替换代码。要看到这一点,让我们看下面的例子: ```cpp int main(void) { auto ptr = new int; } ``` 在前面的例子中,我们使用`new()`分配了一个简单的指针。现在,我们可以查看结果编译的程序集,可以在下面的截图中看到: ![](img/b091f70c-4180-4aa8-a75b-8d820a238fbb.png) 如下图截图所示,对`_Znwm`进行了一次调用,这是对`operator new(unsigned long)`进行了撕裂的 C++ 代码,很容易解缠: ![](img/c027acee-fceb-4dbb-89c2-3bd138eecd8c.png) `new()`运算符本身看起来像下面的伪代码(注意,这没有考虑到禁用异常支持或为新处理程序提供支持的能力): ```cpp void* operator new(size_t size) { if (auto ptr = malloc(size)) { return ptr; } throw std::bad_alloc(); } ``` 现在,我们可以看看新的操作符,看到`malloc()`被调用: ![](img/837ddc0c-aa39-4480-a9dd-618520a38d5f.png) 如前面截图所示,调用`malloc()`。如果得到的指针不是`NULL`,操作员返回;否则,它将进入错误状态,这涉及到调用新的处理程序并最终抛出`std::bad_alloc()`(至少在默认情况下)。 对`malloc()`的调用本身要复杂得多。当一个应用本身启动时,它做的第一件事就是保留堆空间。操作系统给每个应用一个连续的虚拟内存块进行操作,Linux 上的堆是应用中的最后一块内存(即`new()`返回的内存来自应用内存空间的末端)。将堆放在这里为操作系统提供了一种根据需要向应用添加额外内存的方法(因为操作系统只是扩展了应用的虚拟内存)。 应用本身使用`sbrk()`函数,在内存耗尽时向操作系统请求更多内存。调用此函数时,操作系统从其内部页面池中分配页面内存,并通过移动应用内存空间的末尾将此内存映射到应用中。映射过程本身很慢,因为操作系统不仅必须从池中分配页面,这需要某种搜索和保留逻辑,而且还必须遍历应用的页面表,以将这些额外的内存添加到其虚拟地址空间中。 一旦`sbrk()`为应用提供了额外的内存,`malloc()`引擎就会接管。正如我们前面提到的,操作系统只是将内存页面映射到应用中。根据请求的不同,每个页面可以小到 4k 字节,大到 2 MB 甚至 1 GB。然而,在我们的例子中,我们分配了一个简单的整数,它的大小只有`4`字节。为了在不浪费内存的情况下将页面转换成小对象,`malloc()`本身有一种算法,可以将操作系统提供的内存分解成小块。该引擎还必须处理何时释放这些内存块,以便它们可以再次使用。这需要复杂的数据结构来管理应用的所有内存,对`malloc()`、`free()`、`new()`和`delete()`的每次调用都必须运用这一逻辑。 使用`std::make_unique()`创建`std::unique_ptr`的简单调用必须使用从`new()`分配的内存创建`std::unique_ptr`,而`new()`实际上是调用`malloc()`,它必须在复杂的数据结构中搜索,以找到最终可以返回的空闲内存块,也就是说,假设`malloc()`有空闲内存,并且不必使用`sbrk()`向操作系统请求更多内存。 换句话说,动态(即堆)内存很慢,应该只在需要的时候使用,理想情况下,不要在时间关键的代码中使用。 ================================================ FILE: docs/adv-cpp-prog-cb/11.md ================================================ # 十一、C++ 中的常见模式 在本章中,您将学习 C++ 中的各种设计模式。设计模式为正确解决不同类型的问题提供了一种通用的方法,通常,设计模式会在互联网上、会议上以及工作中的饮水机前讨论它们的优缺点。 本章的目标是向您介绍一些更流行、不太流行甚至有争议的模式,让您了解设计模式试图解决的不同类型的问题。这是重要的一章,因为它将教会你解决困难问题的技巧,教会你解决别人过去经历过的常见问题的现有方法。当你在自己的应用中遇到问题时,学习这些设计模式的子集将为自己发现其他设计模式奠定基础。 本章中的配方如下: * 学习工厂模式 * 正确使用单一模式 * 用装饰器模式扩展您的对象 * 添加与观察者模式的通信 * 利用静态多态性提高性能 # 技术要求 要编译和运行本章中的示例,您必须拥有运行 Ubuntu 18.04 的计算机的管理权限,并且具有功能性互联网连接。在运行这些示例之前,您必须安装以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 如果这安装在 Ubuntu 18.04 以外的任何操作系统上,则需要 GCC 7.4 或更高版本以及 CMake 3.6 或更高版本。 本章的代码文件可以在[https://github . com/packt publishing/Advanced-CPP-cook book/tree/master/chapter 11](https://github.com/PacktPublishing/Advanced-CPP-CookBook/tree/master/chapter11)找到。 # 学习工厂模式 在这个食谱中,我们将学习工厂模式是什么,如何实现,以及何时使用。这个方法很重要,尤其是在单元测试中,因为工厂模式提供了添加接缝的能力(也就是说,代码中的有意位置提供了进行更改的机会),能够更改另一个对象分配的对象类型,包括为测试分配假对象的能力。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 执行以下步骤来尝试工厂模式的代码: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter11 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe01_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe01_example01 > ./recipe01_example02 > ./recipe01_example03 correct answer: The answer is: 42 > ./recipe01_example04 wrong answer: Not sure > ./recipe01_example05 correct answer: The answer is: 42 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能,以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 工厂模式为分配资源的对象提供了一种改变对象分配的类型的方法。为了更好地理解这种模式是如何工作的,以及它为什么如此有用,让我们看下面的例子: ```cpp class know_it_all { public: auto ask_question(const char *question) { (void) question; return answer("The answer is: 42"); } }; ``` 如前面的代码所示,我们从一个名为`know_it_all`的类开始,当被问及一个问题时,它会提供一个答案。在这种特殊情况下,无论问什么问题,它总是返回相同的答案。答案定义如下: ```cpp class answer { std::string m_answer; public: answer(std::string str) : m_answer{std::move(str)} { } }; ``` 如前所示,答案是一个简单的类,它被构造为一个字符串,并在内部存储该字符串。需要注意的是,在这种情况下,该 API 的用户实际上不能提取答案类存储的字符串,这意味着这些 API 的使用如下: ```cpp int main(void) { know_it_all universe; auto ___ = universe.ask_question("What is the meaning of life?"); } ``` 如前所示,我们可以问一个问题,并提供一个结果,但我们不确定实际提供了什么结果。这种类型的问题在面向对象编程中一直存在,测试这种逻辑是为什么整本书都以对象嘲讽为主题的众多原因之一。模拟是一个对象的假版本,专门设计用于验证测试的输出(不像假的,它只不过是一个提供测试输入的对象)。然而,在前面的例子中,模拟仍然需要一种方法来创建,以便验证函数的输出。输入工厂模式。 让我们按如下方式修改答案类: ```cpp class answer { std::string m_answer; public: answer(std::string str) : m_answer{std::move(str)} { } static inline auto make_answer(std::string str) { return answer(str); } }; ``` 如前面的代码所示,我们添加了一个静态函数,允许`answer`类创建自身的实例。我们没有改变这样一个事实,即`answer`类不提供提取其内容的能力,只是`answer`类是如何创建的。然后我们可以修改`know_it_all`类如下: ```cpp template class know_it_all { public: auto ask_question(const char *question) { (void) question; return factory("The answer is: 42"); } }; ``` 如前面的代码所示,这里唯一的区别是`know_it_all`类为`factory_t`取了一个模板参数,并使用它来创建答案类,而不是直接创建`answer`类。`factory_t`定义如下: ```cpp using factory_t = answer(*)(std::string str); ``` 这默认为我们添加到`answer`类的静态`make_answer()`函数。前面的例子以最简单的形式演示了工厂模式。我们不直接创建对象,而是将对象的创建委托给另一个对象。前面的实现没有改变这两个类的使用方式,如下所示: ```cpp int main(void) { know_it_all universe; auto ___ = universe.ask_question("What is the meaning of life?"); } ``` 如上图所示,`main()`逻辑保持不变,但这种新方法确保了`know_it_all`类专注于回答问题,而不用担心如何创建`answer`类本身,将该任务留给不同的对象。这种微妙变化背后的真正力量是,我们现在可以为`know_it_all`级提供不同的工厂,导致不同的`answer`级被退回。为了演示这一点,让我们创建一个新的`answer`类,如下所示: ```cpp class expected_answer : public answer { public: expected_answer(std::string str) : answer{str} { if (str != "The answer is: 42") { std::cerr << "wrong answer: " << str << '\n'; exit(1); } std::cout << "correct answer: " << str << '\n'; } static inline answer make_answer(std::string str) { return expected_answer(str); } }; ``` 如上图所示,我们已经创建了一个新的`answer`类,它对原来的`answer`类进行了子分类。这个新类检查它在构造过程中给出的值,并根据它提供的字符串输出成功或失败。我们可以如下使用这个新的`answer`类: ```cpp int main(void) { know_it_all universe; auto ___ = universe.ask_question("What is the meaning of life?"); } ``` 以下是结果输出: ![](img/41a225db-9424-4bd1-aa9f-8be824d88b8d.png) 使用前面的方法,我们不需要修改原始的`answer`类,就不能问不同的问题来查看`know_it_all`类是否提供了正确的答案。例如,假设`know_it_all`类是这样实现的: ```cpp template class know_it_all { public: auto ask_question(const char *question) { (void) question; return factory("Not sure"); } }; ``` 我们测试了这个版本的`know_it_all`类,如下所示: ```cpp int main(void) { know_it_all universe; auto ___ = universe.ask_question("What is the meaning of life?"); } ``` 结果如下: ![](img/3626806f-b78e-4c07-81cf-329507c18135.png) 需要注意的是,工厂模式有几种实现方式。前面的方法使用模板参数来改变`know_it_all`类创建答案的方式,但是我们也可以使用运行时方法,如本例所示: ```cpp class know_it_all { std::function m_factory; public: know_it_all(answer(*f)(std::string str) = answer::make_answer) : m_factory{f} { } auto ask_question(const char *question) { (void) question; return m_factory("The answer is: 42"); } }; ``` 如上图所示,我们从一个自定义的`know_it_all`构造函数开始,该构造函数存储一个工厂函数的指针,同样默认为我们的`answer`类,但是如果我们选择的话,它提供了更改工厂的能力,如下图所示: ```cpp int main(void) { know_it_all universe(expected_answer::make_answer); auto ___ = universe.ask_question("What is the meaning of life?"); } ``` 如果我们愿意,我们还可以在这个类中添加一个 setter,以便在运行时更改这个函数指针。 # 正确使用单一模式 在这个食谱中,我们将学习如何在 C++ 11 和更高版本中正确地实现单例模式,以及什么时候使用单例模式是合适的。这个方法很重要,因为它将教你何时使用单例模式,该模式提供了单个全局资源的明确定义,确保资源保持全局,而不可能有多个副本。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 执行以下步骤来尝试单例模式: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter11 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe01_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe02_example01 memory: 0x4041a0 i1: 0x4041a0 i2: 0x4041a4 i3: 0x4041a8 i4: 0x4041ac > ./recipe02_example02 memory: 0x4041a0 i1: 0x4041a0 i2: 0x4041a4 i3: 0x4041a0 i4: 0x4041a4 > ./recipe02_example03 memory: 0x4041a0 i1: 0x4041a0 i2: 0x4041a4 i3: 0x4041a8 i4: 0x4041ac > ./recipe02_example04 memory: 0x4041a0 i1: 0x4041a0 i2: 0x4041a4 i3: 0x4041a8 i4: 0x4041ac ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 单例模式在 C++ 中已经存在了几年,可以说它是所有 C++ 中最有争议的模式之一,因为它的全局特性在您的应用中引入了耦合(类似于全局变量如何引入耦合)。单例模式实现了一个单一的全局资源。具体来说,它创建了一个保持全局范围的对象,同时确保不存在自身的副本。关于是否应该在您的代码中使用单例模式的争论不会在本书中得到回答,因为这取决于您的用例,但是让我们至少讨论一下这种模式的一些优点和缺点。 **优势:**单体模式为只能包含单个实例的全局资源提供了一个明确定义的接口。不管我们喜欢与否,全局资源存在于我们所有的应用中(例如,堆内存)。如果需要这样的全局资源,并且您有一个处理耦合的机制(例如,像希波莫克这样的模仿引擎),那么单例模式是确保全局资源得到正确管理的好方法。 **缺点:**以下是缺点: * 单例模式定义了一个全局资源,像任何全局资源(例如,一个全局变量)一样,任何使用单例对象的代码都与单例紧密耦合。在面向对象的设计中,应该总是避免耦合,因为它防止了伪造代码可能依赖的资源的能力,这限制了测试时的灵活性。 * 单一模式隐藏了依赖性。检查对象的接口时,无法确定对象的实现是否依赖于全局资源。大多数人认为这可以用好的文档来处理。 * 单例模式在应用的整个生命周期中保持其状态。当单元测试作为单例的状态从一个单元测试进行到下一个单元测试时,尤其如此(也就是说,缺点是显而易见的),大多数人认为这违反了什么是单元测试。 总的来说,应该始终避免全球资源。句号。确保您的代码被正确地编写,以便在需要单个全局资源时强制执行单例设计模式。让我们讨论下面的例子。 假设您正在为嵌入式设备编写应用,并且您的嵌入式设备有一个额外的内存池,可以映射到您的应用中(例如,视频或网络设备的设备内存)。现在,假设您只能拥有这些额外的内存池中的一个,并且您需要实现一组 API 来从这个池中分配内存。在我们的示例中,我们将使用以下内容实现这个内存池: ```cpp uint8_t memory[0x1000] = {}; ``` 接下来,我们将实现一个内存管理器类来分配该池中的内存,如下所示: ```cpp class mm { uint8_t *cursor{memory}; public: template T *allocate() { if (cursor + sizeof(T) > memory + 0x1000) { throw std::bad_alloc(); } auto ptr = new (cursor) T; cursor += sizeof(T); return ptr; } }; ``` 如前面的代码所示,我们已经创建了一个内存管理器类,该类存储了一个指向内存缓冲区的指针,该缓冲区包含我们的单个全局资源。然后,我们创建一个简单的分配函数,根据需要处理这些内存(没有释放能力,这使得算法非常简单)。 由于这是一个全局资源,我们按如下方式全局创建该类: ```cpp mm g_mm; ``` 最后,我们可以使用新的内存管理器,如下所示: ```cpp int main(void) { auto i1 = g_mm.allocate(); auto i2 = g_mm.allocate(); auto i3 = g_mm.allocate(); auto i4 = g_mm.allocate(); std::cout << "memory: " << (void *)memory << '\n'; std::cout << "i1: " << (void *)i1 << '\n'; std::cout << "i2: " << (void *)i2 << '\n'; std::cout << "i3: " << (void *)i3 << '\n'; std::cout << "i4: " << (void *)i4 << '\n'; } ``` 在前面的示例中,我们分配了四个整数指针,然后输出内存块的地址和整数指针的地址,以确保算法按预期工作,从而产生以下输出: ![](img/4538b81d-108f-4a76-98a9-f759ccfadcff.png) 如前所示,内存管理器会根据需要正确分配内存。 前面实现的问题是,内存管理器只是一个类,就像任何其他类一样,这意味着它可以被创建任意多次,也可以被复制。为了更好地说明为什么这是一个问题,让我们看看下面的例子。让我们创建两个内存管理器,而不是创建一个: ```cpp mm g_mm1; mm g_mm2; ``` 接下来,让我们按如下方式使用这两种内存管理器: ```cpp int main(void) { auto i1 = g_mm1.allocate(); auto i2 = g_mm1.allocate(); auto i3 = g_mm2.allocate(); auto i4 = g_mm2.allocate(); std::cout << "memory: " << (void *)memory << '\n'; std::cout << "i1: " << (void *)i1 << '\n'; std::cout << "i2: " << (void *)i2 << '\n'; std::cout << "i3: " << (void *)i3 << '\n'; std::cout << "i4: " << (void *)i4 << '\n'; } ``` 如前所示,唯一的区别是我们现在使用两个内存管理器,而不是一个。这将产生以下输出: ![](img/8a5fd1a4-19c6-44c5-8250-18a3e61714ec.png) 如前所示,内存已被双重分配,这可能会导致损坏和未定义的行为。出现这种情况的原因是内存缓冲区本身是一种全局资源,这是我们无法改变的。内存管理器本身没有做任何事情来确保这种情况不会发生,因此,该 API 的用户可能会意外地创建第二个内存管理器。请注意,在我们的示例中,我们显式地创建了第二个副本,但是第二个副本可能通过简单地传递内存管理器而出现,在此过程中无意中创建了副本。 为了解决这个问题,我们必须处理两个特定的场景: * 创建多个内存管理器实例 * 复制内存管理器 为了解决这两个问题,现在让我们展示单例模式: ```cpp class mm { uint8_t *cursor{memory}; mm() = default; ``` 如上图所示,我们从标记为`private`的构造函数开始。将构造函数标记为`private`可以防止内存管理器创建自己的内存管理器实例。相反,要获得内存管理器的实例,我们将使用以下`public`函数: ```cpp static auto &instance() { static mm s_mm; return s_mm; } ``` 前面的这个函数创建了内存管理器的静态(即全局)实例,然后返回对这个实例的引用。使用这个函数,API 的用户只能从这个函数获得内存管理器的一个实例,这个函数总是只返回对全局定义资源的引用。换句话说,没有编译器的抱怨,就无法创建额外的类实例。 创建单例类的最后一步如下: ```cpp mm(const mm &) = delete; mm &operator=(const mm &) = delete; mm(mm &&) = delete; mm &operator=(mm &&) = delete; ``` 如前所示,复制和移动构造函数/运算符被显式删除。这解决了第二个问题。通过移除复制构造函数和运算符,无法创建全局资源的副本,从而确保该类仅作为单个全局对象存在。 为了使用这个单例类,我们将执行以下操作: ```cpp int main(void) { auto i1 = mm::instance().allocate(); auto i2 = mm::instance().allocate(); auto i3 = mm::instance().allocate(); auto i4 = mm::instance().allocate(); std::cout << "memory: " << (void *)memory << '\n'; std::cout << "i1: " << (void *)i1 << '\n'; std::cout << "i2: " << (void *)i2 << '\n'; std::cout << "i3: " << (void *)i3 << '\n'; std::cout << "i4: " << (void *)i4 << '\n'; } ``` 这将产生以下输出: ![](img/cde66b36-11d4-4296-b84d-0a76d9e7da36.png) 如果我们尝试自己创建内存管理器的另一个实例,我们会得到类似于下面的错误: ```cpp /home/user/book/chapter11/recipe02.cpp:166:4: error: ‘constexpr mm::mm()’ is private within this context 166 | mm g_mm; ``` 最后,由于 singleton 类是一个单一的全局资源,我们可以创建包装器来消除冗长,如下所示: ```cpp template constexpr T *allocate() { return mm::instance().allocate(); } ``` 这种变化可以如下使用: ```cpp int main(void) { auto i1 = allocate(); auto i2 = allocate(); auto i3 = allocate(); auto i4 = allocate(); std::cout << "memory: " << (void *)memory << '\n'; std::cout << "i1: " << (void *)i1 << '\n'; std::cout << "i2: " << (void *)i2 << '\n'; std::cout << "i3: " << (void *)i3 << '\n'; std::cout << "i4: " << (void *)i4 << '\n'; } ``` 如前所示,`constexpr`包装器提供了一种简单的方法来消除我们的单例类的冗长性,如果内存管理器不是单例的话,这是很难做到的。 # 用装饰器模式扩展您的对象 在本食谱中,我们将学习如何实现装饰器模式,该模式提供了扩展类功能的能力,而不需要继承,继承本质上是静态的。这个方法很重要,因为继承不支持在运行时扩展类的能力,装饰器模式解决了这个问题。 # 准备好 开始之前,请确保满足所有技术要求,包括安装 Ubuntu 18.04 或更高版本,并在终端窗口中运行以下内容: ```cpp > sudo apt-get install build-essential git cmake ``` 这将确保您的操作系统拥有适当的工具来编译和执行本食谱中的示例。完成后,打开一个新的终端。我们将使用这个终端来下载、编译和运行我们的示例。 # 怎么做... 执行以下步骤来尝试此食谱: 1. 从新的终端,运行以下命令下载源代码: ```cpp > cd ~/ > git clone https://github.com/PacktPublishing/Advanced-CPP-CookBook.git > cd Advanced-CPP-CookBook/chapter11 ``` 2. 要编译源代码,请运行以下命令: ```cpp > cmake . > make recipe03_examples ``` 3. 编译源代码后,您可以通过运行以下命令来执行该配方中的每个示例: ```cpp > ./recipe03_example01 button width: 42 > ./recipe03_example02 button1 width: 10 button2 width: 42 > ./recipe03_example03 button width: 74 > ./recipe03_example04 button width: 42 button content width: 4 ``` 在下一节中,我们将逐一介绍这些示例,并解释每个示例程序的功能以及它与本食谱中所教授的课程之间的关系。 # 它是如何工作的... 在这个食谱中,我们将学习如何实现装饰器模式。首先,让我们看一个简单的例子:假设我们正在编写一个 C++ 应用,它将托管一个网站。在我们的网站中,我们需要定义一个用户可以点击的按钮,但我们需要计算按钮的宽度,给定一个额外的边距,增加按钮的总大小: ```cpp class margin { public: int width() { return 32; } }; ``` 如前所示,我们已经创建了一个名为`margin`的类,它返回所讨论的边距的宽度(为了简化我们的示例,我们将只关注宽度)。然后,我们可以如下定义我们的按钮: ```cpp class button : public margin { public: int width() { return margin::width() + 10; } }; ``` 如上图所示,我们按钮的总宽度是按钮本身的宽度加上边距的宽度。然后我们可以得到按钮的宽度,如下所示: ```cpp int main() { auto b = new button(); std::cout << "button width: " << b->width() << '\n'; } ``` 这将产生以下输出: ![](img/697bea8a-1cdb-45ac-8431-53344285e825.png) 前面示例的问题是按钮必须始终有边距,因为按钮直接继承了边距类。有一些方法可以防止这种情况(例如,我们的按钮可以有一个配置选项来确定按钮是否返回有边距的宽度),但是在这个食谱中,我们将使用装饰器模式来解决这个问题,允许我们创建两个按钮:一个有边距的按钮,一个没有边距的按钮。让我们试试这个: 1. 首先,让我们定义如下纯虚拟基类: ```cpp class base { public: virtual int width() = 0; }; ``` 如上图所示,纯虚拟基类定义了`width`函数。 2. 然后,我们可以如下实现我们的按钮: ```cpp class button : public base { public: int width() override { return 10; } }; ``` 如上图所示,按钮继承基类并返回宽度`10`。利用前面的,我们可以开始`button`永远是`10`的宽度,按钮没有边距的概念。 3. 要给按钮添加边距,我们首先必须创建一个装饰器类,如下所示: ```cpp class decorator : public base { std::unique_ptr m_base; public: decorator(std::unique_ptr b) : m_base{std::move(b)} { } int width() { return m_base->width(); } }; ``` 装饰器模式从指向`base`指针的私有成员开始,该指针在装饰器的构造函数中设置。装饰器还定义了`width`函数,但是将调用转发给基类。 4. 现在,我们可以创建一个 margin 类,它是一个装饰器,如下所示: ```cpp class margin : public decorator { public: margin(std::unique_ptr b) : decorator{std::move(b)} { } int width() { return decorator::width() + 32; } }; ``` 如上图所示,margin 类返回添加了附加的`32`的对象的宽度。 5. 然后,我们可以如下创建两个按钮: ```cpp int main() { auto button1 = std::make_unique