├── .markdownlint.jsonc ├── Readme.md ├── en-US ├── calling-for-language-features.md ├── collected-issue-clarifications.md ├── contents.md ├── evaluation-order-comments.md ├── integer-type-reasoning.md └── licensing.md └── zh-CN ├── OO-and-procedural-oriented-mist.md ├── about-garbage-collection.md ├── about-intelligence.md ├── about-interface.md ├── about-operating-systems.md ├── about-syntax.md ├── authority-and-confusion.md ├── c-cpp-fundamental.md ├── c-cpp-generic-type-system-defect.md ├── c-wrongs.md ├── c11-cpp11-assignment.md ├── combinator-critique.md ├── computation-and-physics.md ├── contents.md ├── cpp-design-defect.md ├── cpp-exceptions.md ├── cpp-term-translation-comment.md ├── criticisms-on-UTF-8-everywhere-manifesto.md ├── elementary-notes ├── 00-about.md └── contents.md ├── font-rendering.md ├── high-quality-c-cpp-programing-guide-trap-2.md ├── high-quality-c-cpp-programing-guide-trap.md ├── introduction-to-learning-computer-languages.md ├── learning-c-programing-note.md ├── main-function.md ├── mingw-vs-mingw-v64.md ├── move-vs-copy.md ├── philosophy-of-make-decision.md ├── string-and-string-length.md ├── terms-and-bibliography.md ├── typing-vs-typechecking.md ├── variables.md ├── what-is-syntax.md ├── why-i-dislike-apple.md └── why-is-pointer-awful.md /.markdownlint.jsonc: -------------------------------------------------------------------------------- 1 | /* 2 | © 2024 FrankHB. 3 | 4 | This file is part of the YSLib project, and may only be used, modified, 5 | and distributed under the terms of the YSLib project license. 6 | By continuing to use, modify, or distribute this file you indicate that 7 | you have read the license and understand and accept it fully. 8 | */ 9 | 10 | // NOTE: See https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.jsonc 11 | // for the example configuration. 12 | { 13 | 14 | // Default state for all rules 15 | "default": true, 16 | 17 | // Path to configuration file to extend 18 | "extends": null, 19 | 20 | // MD001/heading-increment : Heading levels should only increment by one level at a time : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md001.md 21 | "MD001": true, 22 | 23 | // MD003/heading-style : Heading style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md003.md 24 | "MD003": 25 | { 26 | // Heading style 27 | //"style": "consistent" 28 | "style": "atx" 29 | }, 30 | 31 | // MD004/ul-style : Unordered list style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md004.md 32 | "MD004": 33 | { 34 | // List style 35 | "style": "consistent" 36 | }, 37 | 38 | // MD005/list-indent : Inconsistent indentation for list items at the same level : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md005.md 39 | "MD005": true, 40 | 41 | // MD007/ul-indent : Unordered list indentation : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md007.md 42 | "MD007": 43 | { 44 | // Spaces for indent 45 | //"indent": 2, 46 | "indent": 1, 47 | // Whether to indent the first level of the list 48 | "start_indented": false, 49 | // Spaces for first level indent (when start_indented is set) 50 | "start_indent": 2 51 | }, 52 | 53 | // MD009/no-trailing-spaces : Trailing spaces : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md009.md 54 | "MD009": 55 | { 56 | // Spaces for line break 57 | "br_spaces": 2, 58 | // Allow spaces for empty lines in list items 59 | "list_item_empty_lines": false, 60 | // Include unnecessary breaks 61 | //"strict": false 62 | "strict": true 63 | }, 64 | 65 | // MD010/no-hard-tabs : Hard tabs : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md010.md 66 | /* 67 | "MD010": 68 | { 69 | // Include code blocks 70 | "code_blocks": true, 71 | // Fenced code languages to ignore 72 | "ignore_code_languages": [ ], 73 | // Number of spaces for each hard tab 74 | "spaces_per_tab": 1 75 | }, 76 | */ 77 | "MD010": false, 78 | 79 | // MD011/no-reversed-links : Reversed link syntax : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md011.md 80 | "MD011": true, 81 | 82 | // MD012/no-multiple-blanks : Multiple consecutive blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md012.md 83 | "MD012": 84 | { 85 | // Consecutive blank lines 86 | //"maximum": 1 87 | "maximum": 2 88 | }, 89 | 90 | // MD013/line-length : Line length : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md013.md 91 | "MD013": 92 | { 93 | // Number of characters 94 | //"line_length": 80, 95 | "line_length": 8192, 96 | // Number of characters for headings 97 | "heading_line_length": 80, 98 | // Number of characters for code blocks 99 | //"code_block_line_length": 80, 100 | "code_block_line_length": 8192, 101 | // Include code blocks 102 | "code_blocks": true, 103 | // Include tables 104 | "tables": true, 105 | // Include headings 106 | "headings": true, 107 | // Strict length checking 108 | "strict": false, 109 | // Stern length checking 110 | "stern": false 111 | }, 112 | 113 | // MD014/commands-show-output : Dollar signs used before commands without showing output : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md014.md 114 | "MD014": true, 115 | 116 | // MD018/no-missing-space-atx : No space after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md018.md 117 | "MD018": true, 118 | 119 | // MD019/no-multiple-space-atx : Multiple spaces after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md019.md 120 | "MD019": true, 121 | 122 | // MD020/no-missing-space-closed-atx : No space inside hashes on closed atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md020.md 123 | "MD020": true, 124 | 125 | // MD021/no-multiple-space-closed-atx : Multiple spaces inside hashes on closed atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md021.md 126 | "MD021": true, 127 | 128 | // MD022/blanks-around-headings : Headings should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md022.md 129 | "MD022": 130 | { 131 | // Blank lines above heading 132 | "lines_above": 1, 133 | // Blank lines below heading 134 | "lines_below": 1 135 | }, 136 | 137 | // MD023/heading-start-left : Headings must start at the beginning of the line : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md023.md 138 | "MD023": true, 139 | 140 | // MD024/no-duplicate-heading : Multiple headings with the same content : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md024.md 141 | "MD024": 142 | { 143 | // Only check sibling headings 144 | //"allow_different_nesting": false, 145 | // Only check sibling headings 146 | //"siblings_only": false 147 | "siblings_only": true 148 | }, 149 | 150 | // MD025/single-title/single-h1 : Multiple top-level headings in the same document : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md025.md 151 | /* 152 | "MD025": 153 | { 154 | // Heading level 155 | "level": 1, 156 | // RegExp for matching title in front matter 157 | "front_matter_title": "^\\s*title\\s*[:=]" 158 | }, 159 | */ 160 | // NOTE: This is currently not enforced. It can be useful for the single 161 | // document, but not always when the documents are the parts of a 162 | // collection of documents. Although it might be considered necessary in a 163 | // book, currently not all online documents (even using `rustdoc` or 164 | // `mdbook`) are in this style. 165 | "MD025": false, 166 | 167 | // MD026/no-trailing-punctuation : Trailing punctuation in heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md026.md 168 | "MD026": 169 | { 170 | // Punctuation characters 171 | "punctuation": ".,;:!。,;:!" 172 | }, 173 | 174 | // MD027/no-multiple-space-blockquote : Multiple spaces after blockquote symbol : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md027.md 175 | "MD027": true, 176 | 177 | // MD028/no-blanks-blockquote : Blank line inside blockquote : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md028.md 178 | "MD028": true, 179 | 180 | // MD029/ol-prefix : Ordered list item prefix : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md029.md 181 | "MD029": 182 | { 183 | // List style 184 | "style": "one_or_ordered" 185 | }, 186 | 187 | // MD030/list-marker-space : Spaces after list markers : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md030.md 188 | "MD030": 189 | { 190 | // Spaces for single-line unordered list items 191 | "ul_single": 1, 192 | // Spaces for single-line ordered list items 193 | "ol_single": 1, 194 | // Spaces for multi-line unordered list items 195 | "ul_multi": 1, 196 | // Spaces for multi-line ordered list items 197 | "ol_multi": 1 198 | }, 199 | 200 | // MD031/blanks-around-fences : Fenced code blocks should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md031.md 201 | "MD031": 202 | { 203 | // Include list items 204 | "list_items": true 205 | }, 206 | 207 | // MD032/blanks-around-lists : Lists should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md032.md 208 | "MD032": true, 209 | 210 | // MD033/no-inline-html : Inline HTML : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md033.md 211 | "MD033": 212 | { 213 | // Allowed elements 214 | "allowed_elements": ["sub", "sup", "tt"] 215 | }, 216 | 217 | // MD034/no-bare-urls : Bare URL used : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md034.md 218 | "MD034": true, 219 | 220 | // MD035/hr-style : Horizontal rule style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md035.md 221 | "MD035": 222 | { 223 | // Horizontal rule style 224 | "style": "consistent" 225 | }, 226 | 227 | // MD036/no-emphasis-as-heading : Emphasis used instead of a heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md036.md 228 | /* 229 | "MD036": 230 | { 231 | // Punctuation characters 232 | "punctuation": ".,;:!?。,;:!?" 233 | }, 234 | */ 235 | // XXX: It seems false postive when the punctuation is full-with. 236 | "MD036": false, 237 | 238 | // MD037/no-space-in-emphasis : Spaces inside emphasis markers : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md037.md 239 | "MD037": true, 240 | 241 | // MD038/no-space-in-code : Spaces inside code span elements : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md038.md 242 | "MD038": true, 243 | 244 | // MD039/no-space-in-links : Spaces inside link text : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md039.md 245 | "MD039": true, 246 | 247 | // MD040/fenced-code-language : Fenced code blocks should have a language specified : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md040.md 248 | "MD040": 249 | { 250 | // List of languages 251 | "allowed_languages": [ ], 252 | // Require language only 253 | "language_only": false 254 | }, 255 | 256 | // MD041/first-line-heading/first-line-h1 : First line in a file should be a top-level heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md041.md 257 | "MD041": 258 | { 259 | // Heading level 260 | "level": 1, 261 | // RegExp for matching title in front matter 262 | "front_matter_title": "^\\s*title\\s*[:=]" 263 | }, 264 | 265 | // MD042/no-empty-links : No empty links : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md042.md 266 | "MD042": true, 267 | 268 | // MD043/required-headings : Required heading structure : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md043.md 269 | /* 270 | "MD043": 271 | { 272 | // List of headings 273 | "headings": [ ], 274 | // Match case of headings 275 | "match_case": false 276 | }, 277 | */ 278 | // NOTE: This is not enabled by default. 279 | "MD043": false, 280 | 281 | // MD044/proper-names : Proper names should have the correct capitalization : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md044.md 282 | "MD044": 283 | { 284 | // List of proper names 285 | "names": [ ], 286 | // Include code blocks 287 | "code_blocks": true, 288 | // Include HTML elements 289 | "html_elements": true 290 | }, 291 | 292 | // MD045/no-alt-text : Images should have alternate text (alt text) : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md045.md 293 | "MD045": true, 294 | 295 | // MD046/code-block-style : Code block style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md046.md 296 | "MD046": 297 | { 298 | // Block style 299 | "style": "fenced" 300 | }, 301 | 302 | // MD047/single-trailing-newline : Files should end with a single newline character : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md047.md 303 | "MD047": true, 304 | 305 | // MD048/code-fence-style : Code fence style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md048.md 306 | "MD048": 307 | { 308 | // Code fence style 309 | "style": "consistent" 310 | }, 311 | 312 | // MD049/emphasis-style : Emphasis style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md049.md 313 | "MD049": 314 | { 315 | // Emphasis style 316 | "style": "consistent" 317 | }, 318 | 319 | // MD050/strong-style : Strong style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md050.md 320 | "MD050": 321 | { 322 | // Strong style 323 | "style": "consistent" 324 | }, 325 | 326 | // MD051/link-fragments : Link fragments should be valid : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md051.md 327 | "MD051": true, 328 | 329 | // MD052/reference-links-images : Reference links and images should use a label that is defined : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md052.md 330 | "MD052": 331 | { 332 | // Include shortcut syntax 333 | "shortcut_syntax": false 334 | }, 335 | 336 | // MD053/link-image-reference-definitions : Link and image reference definitions should be needed : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md053.md 337 | "MD053": 338 | { 339 | // Ignored definitions 340 | "ignored_definitions": [ 341 | "//" 342 | ] 343 | }, 344 | 345 | // MD054/link-image-style : Link and image style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md054.md 346 | "MD054": 347 | { 348 | // Allow autolinks 349 | "autolink": true, 350 | // Allow inline links and images 351 | "inline": true, 352 | // Allow full reference links and images 353 | "full": true, 354 | // Allow collapsed reference links and images 355 | "collapsed": true, 356 | // Allow shortcut reference links and images 357 | "shortcut": true, 358 | // Allow URLs as inline links 359 | "url_inline": true 360 | } 361 | } 362 | 363 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Programing Language Documentations 2 | 3 | This is a repository about some programing languages and something about their theories (either formal or informal). 4 | 5 | Some documents may contain subjective viewpoints. Some of them may change due to the new discoveries on the related topics. There should be rationales to explain why the author keep these points in the meantime. Missing of the discussion around them is not intended, unless explicitly specified. 6 | 7 | # Rules 8 | 9 | Contents in this repository conform to the [YSLib wiki editing rules](https://frankhb.github.io/YSLib-book/WikiRules.en-US.html). 10 | 11 | # Copyright 12 | 13 | © 2014-2024 [FrankHB](mailto:frankhb1989@gmail.com). 14 | 15 | Except as otherwise specified explicitly, materials in this repository are licensed under following terms: 16 | 17 | [![https://creativecommons.org/licenses/by-sa/4.0/](https://i.creativecommons.org/l/by-sa/4.0/88x31.png "license")](https://creativecommons.org/licenses/by-sa/4.0/) 18 | 19 | This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-sa/4.0/ "CC-BY-SA 4.0"). 20 | 21 | -------------------------------------------------------------------------------- /en-US/collected-issue-clarifications.md: -------------------------------------------------------------------------------- 1 | # Collected issue clarifications 2 | 3 | This document is a collection of points to some wide-spread writings with confusions, false statements or outdated information needing clarification known by the author. It can be referenced by other documents. For this purpose, the titles should be stable to be used as hyperlinks. 4 | 5 | ## Preprocessor identification 6 | 7 | It is probably well-known that [musl libc will not provide feature macros identifying the implementation](https://openwall.com/lists/musl/2013/03/29/13), as stated: 8 | 9 | > It's a bug to assume a certain implementation has particular properties rather than testing. 10 | 11 | This is very wrong and has challenges against the commen sense by obvious reasons in a wider view from both users and maintainers: 12 | 13 | * Assuming a certain implementation has particular properties is legitemate and parts of common practice. 14 | * It is legitemate to have different configurations for different needs of users. 15 | * Such configurations may contain separated sets of features which are opted in. 16 | * Having assumption by nominal (i.e. explicitly provided by named feature sets or named library components) set of configurations from underlying implementations is the usual case in portable software development. 17 | * The identification is a well-established way to gain portability as possible in common practice, esp. for a library as libc. 18 | * Macros with prefix `__STDC` in the names are standardized in ISO C. 19 | * Various macros like `__GLIBC__` are used to identify the implementation of libc. 20 | * There is no sufficient reason for the well-known (and even standard) way to make them "bug". 21 | * The use of identification is also necessary for maintainers to implement the standard features with opted-in configurations. 22 | * For example, a libc implementation conforming to versions of POSIX.1 is likely to have different functions defined as per the value from the expansion of macro `_POSIX_SOURCE`. 23 | * As the author of the implementation, the standard ones `_POSIX_SOURCE` is no different to other macros from the environment, except that such macros are known to be user-provided externally. 24 | * Libraries like Musl libc have no practical alternative strategies to prevent meeting the need of such macros. 25 | * Even without the need for opted-in features, working around the defects in the implementations often need to identify the dependency of specific configuarations (including versions). 26 | * The workaround may be in the status of best effort. Unnecessary workaround may decrease the quality for other implementations. 27 | * Rejection of providing a way in this means undermines the portability and quality of implementation in practice (if not totally failed to provide them), because the user cannot detect the required information to distinguish the implementation reliably at all. 28 | * Either the implementation is unsupported, or it is in the `#else` at last, without the capability to be distinguished with other ones also having no idendification macros. 29 | * Testing is a less standard way to the preprocessing macro identification, and often worse compared to alternatives for various reasons (see below), so it cannot replace the latter. 30 | * Identification using the preprocessor, in general, is testing during translation by the C (and others, e.g. C++) implementation, which is almost the most portable form of testing during the build. 31 | * Testing other than identification can be often less reliable. 32 | * They may have significantly worse properties by having false positive and false negative cases. 33 | * Compared to identification, they may be more costly, having more complexity to implement, more difficult to port or reuse in practice. 34 | * Some properties of programs may be undetectable at all in theory. 35 | * See [Rice's theorem](https://en.wikipedia.org/wiki/Rice%27s_theorem) for the general case in the sense of computability. 36 | * Some properties in object language level (i.e. the program code), cannot be reliably covered for the state spaces (a formal parlance of the set of test cases) by normal execution or simulation, instead of formal verification in the meta language level (i.e. the semantic rules of the object language expressed in some formal languaged). 37 | * A canonical example is [PTC](https://www.geeksforgeeks.org/what-is-a-proper-tail-call/). The reason for failure on the coverage by testing is due to the physical infeasibility to construct an environment having unbounded resources for the activation records. 38 | * Practical computational resource boundary (e.g. limited time) may apply (esp. for runtime testing). 39 | * Runtime testing, even feasible, may often have unnecessary performance impacts, even for normally use cases for end-users which have no interest in the topic here at all. 40 | * Configuration-time is out of the standard semantics of programming language implementations, so it can be unavailable at all. 41 | * Even when available, it requires a host environment which requires more assumption on host implementations (e.g. operating systems and shells), hence less portable. 42 | * There are many practical examples showing that lacking identification making less reliable and portable software, as well as making the development experience worse. 43 | * The earlier versions of libstdc++ cannot be detected uniformly, because `__GLIBCXX__` can have versions depending on the distrobution of the GCC package. 44 | * Only [the official (FSF) releases have a well-known mapping from the date to version](https://gcc.gnu.org/onlinedocs/gcc-14.2.0/libstdc++/manual/manual/abi.html#abi.versioning.__GLIBCXX__), and it is still quite inconvenient. 45 | * To solve the problem, [since GCC 7 there is also a `_GLIBCXX_RELEASE` macro](https://gcc.gnu.org/onlinedocs/gcc-14.2.0/libstdc++/manual/manual/using_macros.html). 46 | * Any bug in musl libc will make the detection necessary. 47 | * Here is [an article](https://catfox.life/2022/04/16/the-musl-preprocessor-debate/) discussing how the identification can be useful with some detailed conditions showing that lacking identification makes trouble. 48 | * Here is [a question](https://stackoverflow.com/questions/58177815) showing the existence of real need of detecting musl libc. 49 | 50 | That said, the maintaince of a versioned idenfication in the source code can be more costly, and a software component can have less need of such identification for various: 51 | 52 | * The component is a module in a larger software component, and it will be upgraded together with the module, so an individual idenficiation can be redundant. 53 | * In such cases, the addtional identification may even be harmful due to confusion. 54 | * Users agree that alternative identification or detection method (like testing) is sufficient, when: 55 | * The component is deployed in some controlled or private environment. 56 | * The component provides its API design and implementation together and there is no need to distinguish different implementations (opposed to the libc cases which are the implementations to ISO C and POSIX.1), so it will have few portability impact. 57 | * Version detection may still be useful. 58 | * Runtime detection (by providing specific APIs) may suffice for the purpose of version detection which can be configured by build command (e.g. something like `-DVERSION=1` in te GCC command line) instead of updating the versions in the source code regularly. 59 | 60 | However, neither of them applies to musl libc. Lacking the identification is of a problem of poor QoI and the decision to reject the identification is a bad idea of design in practical software engineering. 61 | 62 | -------------------------------------------------------------------------------- /en-US/contents.md: -------------------------------------------------------------------------------- 1 | # Contents 2 | 3 | Here is the list of the documents hosted in this repository 4 | 5 | ## General-purposed languages design 6 | 7 | * [Calling for language features](calling-for-language-features.md) - Feature lists of a desired programming language 8 | 9 | ## Pragmatics 10 | 11 | * [Integer type reasoning](integer-type-reasoning.md) - Discussion of the background problem and the reasoning of the decision to picking an integer type in a static manifested language general 12 | 13 | ## C++ 14 | 15 | * [Some comments on refining expression evaluation order of C++](evaluation-order-comments.md) 16 | 17 | ## Background topics 18 | 19 | * [Licensing](licensing.md) 20 | * [Collected issue clarifications](collected-issue-clarifications.md) 21 | 22 | -------------------------------------------------------------------------------- /en-US/evaluation-order-comments.md: -------------------------------------------------------------------------------- 1 | # Some comments on refining expression evaluation order of C++ 2 | 3 | Quoted text are from the referenced material: [P0145R1](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0145r1.pdf). 4 | 5 | ## 1 Introduction 6 | 7 | > Order of expression evaluation is a recurring discussion topic in the C++ community. 8 | 9 | Suspectable. Because ... 10 | 11 | > In a nutshell, given an expression such as `f(a, b, c)`, the order in which the sub-expressions `f`, `a`, `b`, `c` (which are of arbitrary shapes) are evaluated is left unspecified by the standard. If any two of these sub-expressions happen to modify the same object without intervening sequence points, the behavior of the program is undefined.\ 12 | > For instance, the expression `f(i++, i)` where `i` is an integer variable leads to undefined behavior, as does `v[i] = i++`. 13 | 14 | The current rules are already somewhat clear (compared with the proposed rules) and not hard to recite. It seems that there are few people willing spending time on this topic continuously. Novices usually do not try to change the core language rules, and seasoned users usually do not bother to do too much beyond setting up the convention to follow the standard. 15 | 16 | > Even when the behavior is not undefined, the result of evaluating an expression can still be anybody’s guess. Consider the following program fragment: 17 | > 18 | >```cpp 19 | >#include 20 | >int main() { 21 | > std::map m; 22 | > m[0] = m.size(); // #1 23 | >} 24 | >``` 25 | > 26 | > What should the map object `m` look like after evaluation of the statement marked #1? `{{0, 0}}` or `{{0, 1}}` ? 27 | 28 | It should be clear without guess because every time the reader meet `=`, he should trace the dependencies (including evaluation order) trivially in a few milliseconds. Since [CWG222](https://wg21.cmeerw.net/cwg/issue222) is resolved, it should be safe to merge the parsing and tracing passes in one's brain for both overloaded and builtin `=` as long as the left side operand has no insane subexpressions screwing up the value dependencies, which should be easy to keep (away from abuse of macros and "properties", etc). Here the effect of `=` is up to the values of operands uniquely. Note the fragment is *not* the case like `m[m[0] = m.size()] = m.size();`. On the other hand, will it be a problem in practice? Just *do not write obviouosly stupid unreadable code*. 29 | 30 | If it is still indeed annoying for someone, then C-like languages are likely not suitable, please try Lisp-like dialects without such overhead of dealing with infix expressions instead. 31 | 32 | ## 2 A Corroding Problem 33 | 34 | > The traps aren’t just for novices or the careless programmer. 35 | 36 | Not exactly. If one does not know why these fundamental rules are important and why ignorance of them is dangerous, he *is* a novice. If one has no idea to avoid it, he *is* a novice. If one cannot force *himself* to obey the convention to prevent such trap, he *is* careless. The remained problem is how to enforce *others* following the convention, but that is another story. 37 | 38 | > Even if you would like to blame the “excessive” chaining, remember that expressions of the form `std::cout << f() << g() << h()` usually result in chaining, after the overloaded operators have been resolved into function calls. 39 | 40 | It is questionable that how "excessive" comes. There is no clear boundary to classify what is "excessive" chaining based only on syntactic use without specific knowledge of call-side scenes. However, if one have no idea that what would occur in `f()`, `g()` and `h()`, then `std::cout << f() << g() << h()` should be already "excessive" to him. 41 | 42 | > The solution isn’t to avoid chaining. 43 | 44 | Why? This key reason is not given by the document as it should be. 45 | 46 | Avoiding chaining *anywhere* is not intended, but avoiding harmful chaining conventionally *is* feasible in practice, since one can *eventually* avoid them anywhere *when in doubt* (but does not have to). This is similar to expressions that can often with (excessive) parentheses being added around. 47 | 48 | > Rather, it is to fix the problem at the source: refinement of the language rules. 49 | 50 | The reason is still missing. Why the unspecified evaluation order is "the problem at the source"? The real problem is *the existence of improper use* and *damage caused by such use*. Unless one can prove the "source" is strictly implying these problems, the statement is logically unsound. This cannot be achieved because there exist some practical ways (not depending on this proposal) to avoid such problems. 51 | 52 | ## 3 Why now? 53 | 54 | > However, a living and evolving programming language cannot just hold onto inertia. 55 | 56 | True, but again the reason for suggested conclusion is missing. Why unspecified (not "underspecified") evaluation order in general is just "inertia"? 57 | 58 | It should be easy to understand that disallowing evaluation with unspecified order and not providing replacement in general is weakening the expressiveness (about indeterminism) of the language. However the document ignores it totally. 59 | 60 | > The language should support contemporary idioms. 61 | 62 | Questionable. What support can the language provide? How? There are many "contemporary idioms", are all of them deserved to be supported by modifying the language rules? If not, which subset of idioms should be treated like this? 63 | 64 | A more apporiate statement can be: the language should *allow* contemporary idioms, and *encourage* them when there are no obvious defects and superior replacements. 65 | 66 | > For example, using `<<` as insertion operator into a stream is now an elementary idiom. 67 | 68 | False. Using `<<` as insertion operator into a stream is only idiomatic for *formatted insertion* of *standard stream classes* and similarly designed classes, and actually not so *elementary* because it is somewhat domain-specific and less dependent by widely spreaded higher-level usage. 69 | 70 | A proper (indisputable) example of "elementary" idiom can be "using `<` to express strictly partial ordering" (which has been further supported by `std::less` and being depended by widely used associative containers, etc). Another can be using unqualified `swap` to swap contents of two objects, which has been supported in Clause 17 and many other clauses of the standard, rather than than only in a few parts of Clause 27 and Clause 30. 71 | 72 | > So is chaining member function calls. 73 | 74 | Nor are chaining member function calls. The method chaining style and related so-called "fluent interface" are strongly OOP-flavored, and even argubly not a good OOP practice in general in other "OOP" language communities. Since C++ is a multiparadigm language rather than "OOP" one, it is questionable to standardize such a disputable style overall into the language specification (either the language core or the library-wide descriptions). That is, it should even *not be encouraged in general*. BTW, there are other easy-to-use idiomatic(able) way to achieve some main goals of method chaining in FP flavor, with some better properties. 75 | 76 | > The language rules should guarantee that such idioms aren’t programming hazards. 77 | 78 | Since such idioms are not unique, the guarantee is not needed urgently. 79 | 80 | > We have library facilities (e.g. `std::future`) designed to be used idiomatically with chaining. 81 | 82 | Still usable without chaining. 83 | 84 | > Without the guarantee that the obvious order of evaluation for function call and member selection is obeyed, these facilities become traps, source of obscure, hard to track bugs, facile opportunities for vulnerabilities. 85 | 86 | Then forbid such use by fixing the interface, if teaching the correct idiom is too cumbersome. 87 | 88 | > The language should support our programming. 89 | 90 | Only with a clear design and a clean clue in user's mind to reason why (or not) he should take the idiomatic way. 91 | 92 | > The changes suggested below are conservative, pragmatic, with one overriding guiding principle: **effective support for idiomatic C++**. 93 | 94 | Actually they are not conservative because they changed the expression semantics so broadly (though the meaning and the behavior of the programs may keep the same); also not so pragmatic because of the limitation of the idioms they supported. 95 | 96 | The "guiding principle" is not so principle as "guiding" because it is lack of: 97 | 98 | * the guideline to determine which idioms are "idiomatic" enough that even the core language rules can be sacrificed; 99 | * the method of measuring the effectiveness. 100 | 101 | > In particular, when choosing between several alternatives, we look for what will provide better support for existing idioms, what will nurture and sustain new programming techniques. 102 | 103 | This is also methodologically questionable. It seems that "to support of member function calls chaining" has been the ultimate premise even before the principle itself. For example, what about to abandon and to replace it by other idioms? The document said nothing about alternatives on such level. And what kinds of new techniques are expected to be nurtured? 104 | 105 | > The primary focus is on what the programmer reads and writes, in particular in generic codes, not what the compiler internally does according to fairly arcane rules. 106 | 107 | This is probably not feasible, because as different compliers do, different human users may also *always* read and write differently, e.g. to assume and depend on the left-to-right order as possible vs. to assume indeterminate evluation and to depend on explicit sequenced ordering (while there should only be one concise set of rules of the language, of course). The worse thing is, there is no way to refine programers (as conformance to compilers) being away from TIMTOWTDI. So in this case, change of rules can only reflect some of the existed practice, thus the result may be less idiomatic than the status quo, which largely defeats the original purpose. 108 | 109 | > Rather, the driver seat should be given to idioms. 110 | 111 | Which idioms? 112 | 113 | ## 4 A Solution 114 | 115 | > A simple solution would be to require that every expression has a well defined evaluation order. 116 | 117 | This can hardly be simple in C++. The proposed changes of rules also shows it. 118 | 119 | > In summary, the following expressions are evaluated in the order `a`, then `b`, then `c`, then `d`: 120 | > 121 | > 1. `a.b` 122 | > 2. `a->b` 123 | > 3. `a->*b` 124 | > 4. `a(b1, b2, b3)` 125 | > 5. `b @= a` 126 | > 6. `a[b]` 127 | > 7. `a << b` 128 | > 8. `a >> b` 129 | 130 | "b1, b2, b3" seems to be a typo. 131 | 132 | And what about other binary operators? Another trap? 133 | 134 | ## 5 Postfix Increment and Decrement 135 | 136 | > The side effects of unary expressions shall be committed before the next expression (if any) is evaluated if it is part of a binary expression or a function call. 137 | 138 | So a careless writer is still easy to make it a mess. In order to figure out what the expression means, readers may be forced to find which is the "next" expression to be evaluated in these contexts, or they will probably be trapped again. This is even worse than before. 139 | 140 | ## 6 Formal Wording 141 | 142 | It seems that "the value computation and the associates side effects of E1 are sequenced before those of E2" are repeated too many times. 143 | 144 | ## 7 Implmentation Experience and Report 145 | 146 | > **It is worth noting that these results are for the worst case scenario where the op timizers have not yet been updated to be aware of, and take advantage of the new evaluation rules and they are blindly forced to evaluate function calls from left to right.** 147 | 148 | How do the optimizer take the advantage of stricter limitation on imlementation? 149 | 150 | > Based on these experiments, we feel confident recommending the left-to-right evaluation rules for syntactic function calls and in the functional cast notation involving more than one arguments in the argument list. 151 | 152 | This chapter is mostly about experience on specific implementation, concerned with some QoI issues. These experiments do not provide some further information to answer: 153 | 154 | * How to estimate the worst cost to modify a current implementation in general? 155 | * May it affect adding non "traditional" reordering optimization in future as per as-if rules? 156 | * Will it leads to more ABI problems? 157 | 158 | Note that an implementation may even need to modify nothing to be conforming. 159 | 160 | ## 8. Alternate Evaluation Order for Function Calls 161 | 162 | > We do not believe that such a non determinism brings any substantial added optimization benefit, but it does perpetuate the confusion and hazards around order of evaluations in function calls. 163 | 164 | Please prove your belief. 165 | 166 | -------------------------------------------------------------------------------- /en-US/integer-type-reasoning.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FrankHB/pl-docs/a15ba95c54461c028b0441d4cdf6afa219f36a70/en-US/integer-type-reasoning.md -------------------------------------------------------------------------------- /en-US/licensing.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | This is a memo about licensing affairs met in various works of the author. 4 | 5 | **Disclaimer from the author to readers: [INANL](https://en.wikipedia.org/wiki/IANAL). Ask your lawers before you make decisions.** 6 | 7 | # License and licensing 8 | 9 | A [license](https://en.wikipedia.org/wiki/License) is an agreement on the licensed works for specific granted permissions. It is used to transfer the rights from the owner (the licensor) to the reciever (the licensee). 10 | 11 | Licensing is a legal process to specify the license on some works to ensure that the licensee will have the legal rights to interact with such licensed works when the related laws are capable to grant such rights. The laws defining such rights include copyright laws, patent laws, and so on. 12 | 13 | ## How it works 14 | 15 | Although laws may differ in various jurisdictions, many rights are protected similarly. This is implemented by widely adopted international conventions like [Berne Convention](https://en.wikipedia.org/wiki/Berne_Convention). Such laws often reserve the rights to limited owners exclusively by default. 16 | 17 | Technically, a license is intended to be enforceable as a contract. Contract laws make the license clauses be legally effective and the limitation of exclusive rights can be bypassed. This fact makes it as a generally available instrument to grant such rights. 18 | 19 | When the license is unavailable, infringement can easily occur. For example, with the jurisdiction in most areas in the world, users may not copy unlicensed software without a license or extra permissions by default. So, licensing should be substantial for the purpose of legitimation. 20 | 21 | ## Licensee 22 | 23 | The licensee of a license can be specific or uncertain individuals or organizations whichever supported by the laws. 24 | 25 | Both [EULA](https://en.wikipedia.org/wiki/EULA) and public licenses should be similar legal effects. 26 | 27 | It is reported that [one of the mostly used free public license is an enforceable contract in US](https://www.theregister.co.uk/2017/05/13/gnu_gpl_enforceable_contract/). 28 | 29 | # The rights 30 | 31 | Widely adopted licensable targets include copyrights, patents and trademarks. 32 | 33 | Sometimes the rights involved are collectively named as "intellectual property". However, there are [critisms](https://en.wikipedia.org/wiki/Intellectual_property#Criticisms) against the term of use. To avoid ambiguity, this article will avoid the use unless necessary. 34 | 35 | ## Copyrights 36 | 37 | Copyrights are often the main targets to be covered by a license. 38 | 39 | Historically, there are different views of emphasis on the concerned rights involved in modern copyright laws. The traditional Anglo-Saxon concept of "copyright" only deals with economic concerns, contrasting to the French "right of the author" (droit d'auteur). As one of the most important international framework of copyright laws, Berne Convention are influenced by both aspects. 40 | 41 | ### Copyrightable works 42 | 43 | Not all works can be copyrightable. For example, mathematical formulae are usually not the objects and excluded from the copyright laws explicitly. In essense, the theory of programming languages and the spirit of the designs are also not copyrightable. However, documents about such designs are definitely copyrightable. 44 | 45 | Not all rights are qualified to be the legal targets in the license. Licensable copyrights are essentially rights over properties. The rights of authority is stripped away from the allowed list of the rights. 46 | 47 | ### Copyright holders 48 | 49 | [Copyright holders](https://www.csusa.org/page/Definitions#copyrightholder) are owners of copyrights. Note the ownership of the copyrights are separate from the ownership of the copyrighted works, so they can be different at the same time. 50 | 51 | Except as otherwise specified by the laws, only copyright holders can ultimately decide which sets of the rights are included in the license. 52 | 53 | There are some exceptional cases qualified as the "otherwise specified" above. Under some conditions, the copyright laws may provide sections to explicitly grant a relatively narrow set of the rights without permission from the copyright holder, like [fair use](https://en.wikipedia.org/wiki/Fair_use). 54 | 55 | ### Derivation works 56 | 57 | ### Copyleft vs. permissive licenses 58 | 59 | ## Patents 60 | 61 | ## Public domain 62 | 63 | ## Sublicensing vs. relicensing 64 | 65 | The term [sublicensing](https://en.wiktionary.org/wiki/sublicense)" is used to describe the act to license to third parties. Do not confuse it with relicensing (e.g. [software relicensing](https://en.wikipedia.org/wiki/Software_relicensing)), i.e. changing the existing lecense of the licensed works. The latter is mostly preserved to the holders, because arbitrary abilities of relicensing can make the license practically become the declaration of public domain. 66 | 67 | # License compatibility problems 68 | 69 | # History and philosophies 70 | 71 | ## The free software movement 72 | 73 | ## OSI definitions 74 | 75 | ## Policy differences 76 | 77 | # Other media and license frameworks 78 | 79 | ## Creative Common 80 | 81 | ## Doujin works 82 | 83 | # Examples of choices 84 | 85 | ## Copyleft licenses 86 | 87 | ## BSD vs. MIT 88 | 89 | Strictly speaking, [BSD](https://en.wikipedia.org/wiki/BSD_licenses) and [MIT](https://en.wikipedia.org/wiki/MIT_License) are not good names to identify licenses. This make problems particlarly with [BSD](https://www.gnu.org/philosophy/bsd.html) because the versions are confusing. Each version has different conditions to meet. MIT also makes problems because there are at least 2 variants (X11 and Expat) widely used. Such issues are actually [more problematic](https://github.com/licensee/licensee/issues/98) when more variants are considered. 90 | 91 | As stated in the link above, the original 4-clause BSD license has problems of verbosity and compatibility, it should not be used any longer in practice. 92 | 93 | Conceptually 2-clause BSD license (the FreeBSD license) can derive 3-clause BSD license (the NewBSD license) and Expat license can derive X11 license in the same way (by adding the non-endorsement clause). The non-endorsement clause is not necessary since the [purpose](https://softwareengineering.stackexchange.com/questions/41128/what-is-the-purpose-of-the-non-endorsement-clause-in-the-new-bsd-license) (protecting against the endorsement) can be achieved by other means more effectively (e.g. by fraud law). So, only the former ones are compared below. 94 | 95 | Most rights granted by 2-clause BSD license vs. by Expat license are same. 96 | 97 | * Both grants permissions on use, redistribution, redistribution with modification. 98 | * Both requires to retain the copyright notice and warranty disclaimer. 99 | * Both are not copyleft. 100 | * Both have no patent clauses. 101 | 102 | While the license text seems more succint in the latter, here are some other detailed differents: 103 | 104 | * The copyrighted works differ. The term of SOFTWARE in Expat license are considerably restrictive than it in 2-clause BSD license. The notion does not cover some other kinds of works, e.g. hardware design in the form of [Verilog](https://en.wikipedia.org/wiki/Verilog) code. 105 | * The conditions differ. 106 | * 2-clause BSD license allows slightly different conditions on the source code and binary form of the software. Expat license makes the same conditions for the source code and the binary form of the software. 107 | * 2-clause BSD license enforce more explicity the reproduction of the disclaimer clause and the credits. 108 | * There are extra permissions (merging, publishing, sublicensing and selling) specified by Expat license explicitly. However, all can be derived from the text in 2-clause BSD license. 109 | * Expat license insists on "copy" of the license text. Although this is weaker compared with "verbatim copy" wording in licenses like GNU GPL, it is still more restrictive. On the other hand, under BSD-like license, the licensee is permitted to "reproduce" the clauses in other forms, for example, audio files containing the content of the license. The downstream sublicensed licensee can transform such form back to the text. 110 | 111 | Provided with the differences above, the former is considered better for general use in the opinion of the author of this article. 112 | 113 | -------------------------------------------------------------------------------- /zh-CN/OO-and-procedural-oriented-mist.md: -------------------------------------------------------------------------------- 1 | # 面向对象和所谓的“面向过程” 2 | 3 | This is also arranged as a dissertation @ ZJU. 4 | 5 | # 摘要 6 | 7 |   本文综述面向对象,尤其是面向对象编程的基本概念和一些其它编程范型的比较,并指出了现有初学者的一些常见误区。 8 | 9 | # I.面向对象(Object-Oriented, OO) 综述 10 | 11 |   公认的面向对象是一种“思想”,更精确地说是一种*方法学(methodology)* 。*面向对象编程(OOP)* 、*面向对象分析(OOA)* 、*面向对象设计(OOD)* 等范畴是对此的衍生。容易理解,OOP 指使用 OO 的方法进行编程; OOA 和 OOD 分别指使用 OO 的方法进行系统分析与设计(可以合称 OOAD ),是 OO 方法学在软件工程上的应用。对于使用 OO 方法学的软件开发,OOP 是基础也是具有更强的普遍性(即便使用 OO 方法,也并非所有的软件都有必要使用系统化的 OOA 和 OOD 进行开发),通过 OOP 可以反映 OO 在编程实践中的重要应用,因此本文着重论述 OOP 的有关内容。关于 OOAD ,读者可以在了解OOP的基础上自行学习。 12 | 13 | # II.编程范型(programing paradigm) 14 | 15 |   是计算机编程中的一种基本方式[[en.wiki:programming paradigm]](https://en.wikipedia.org/wiki/Programming_paradigm) 。OOP 和*命令式编程(imperative programing)* 、*函数式编程(functional programing)* 、*逻辑编程(logical programing)* 并列,是当前主流的编程范型[[Kurt Nørmarks]](https://people.cs.aau.dk/~normark/prog3-03/html/notes/paradigms_themes-paradigm-overview-section.html)。此外还有*结构化(structured)* 、*声明式(declarative)* 、*面向方面(aspect-oriented)* 、*数据驱动(data-driven)* 、*泛型(generic)* 、*并行(parallel)* 、*元编程(metaprograming)* 等各种范型。 16 | 17 |   应该注意的是,这些范型并不都是同一层次上的风格,且由于分类方法的不同,不都是互斥的。当然也有些范型是对立的:结构化与*非结构化(non-structured)* ,但这是少数。因此通常在同一段程序中使用了一种以上的编程范型,只强调其中的一部分。 18 | 19 | 20 | # III.结构化编程(structued programing) 、命令式编程(imperative programing) 和过程式编程(procedural programing) 21 | 22 |   早期的程序没有强调任何范型,是非结构化的。结构化程序由子程序的(顺序)执行、选择、迭代构成,无需跳转。 23 | 24 |   命令式编程是一种重要的编程范型。它和声明式编程相对,强调特定路径执行的步骤(控制流)使程序的状态向预期改变,而非和执行路径无关的计算逻辑的表达。大部分硬件实现的体系结构都直接支持命令式范型。可执行的语句作为语言特性,是支持命令式编程的重要特征。 25 | 26 |   过程式编程有时被看作是命令式编程的同义词,也可以表示一种基于结构化编程和过程调用(procedual call) 的编程范型[[en.wiki:Procedural programming]](https://en.wikipedia.org/wiki/Procedural_programming)。过程(或例程、子例程、方法、函数——注意不要和函数式编程混淆)在这里是可以通过调用这一手段被重复执行的程序片段,作为语言特性,是支持过程式编程的语言的重要特征。 27 | 28 |   下文中提及的过程式编程都指第二种含义,而第一种直接称为命令式编程。 29 | 30 |   无论是命令式还是过程式的编程范型都被许多编程语言广泛支持。对于 Pascal 、C 、C++ 、Java 等语言,两者都是最基本的(使用时几乎无法避免的)范型,而“结构化”通过这两个范型的上层被体现。 31 | 32 | # IV.作为编程范型(programing paradigm) 的 OOP 33 | 34 |   OO 程序可以看成一系列对象的交互,而不是如传统的过程式编程那样执行一系列任务(*过程* )。 35 | 36 |   如 OO 字面所说,*对象(object)* 是其中的一个要素。 37 | 38 |   OOP 中每个对象都有能力接收、处理和向其它对象发送*消息(message)* ,可以被看作具有不同角色或职责的独立单元。对象的动作(或“*方法(method)* ”)与之紧密相关。例如,OOP 数据结构倾向于携带自身的操作(或至少从类似的对象或类“*继承* ”)。 39 | 40 |   对于过程式编程,程序可以被分解为若干过程。OOP 的做法不同——它分解程序为若干对象和对象的交互(尽管其中的“方法”仍然可以扮演过程式编程中的过程的角色)。 41 | 42 |   传统的过程式编程中过程对数据的访问是不加外部限制的,而OOP的做法不同——它使用访问权限控制等特性,强调封装,鼓励程序员使某一部分的数据仅可被特定的程序片段(过程)访问,以减少可能发生的错误。 43 | 44 |   对象和消息是 OOP 的核心。OOP 和对类型系统的抽象导致*类(class)* 概念的出现。使用类作为核心特性的语言实现 OOP 的*基于类的风格的(class-based style)* OO ,这是支持OOP的语言中的主流,如 C++ 、Java 。与之不同的是基于*原型的风格的(prototype-based style)* OO,如ECMAScript。 45 | 46 |   当前流行的 C++ 、Java 等“主流面向对象语言”(事实上把 C++ 称为面向对象语言并太不恰当,见下文),其实并不能算最符合 OOP 的基本要义。原始的 OOP 的观点,见面向对象之父 Alan Kay 的阐述[[Alan Kay]](https://www.purl.org/stefan_ram/pub/doc_kay_oop_en) 。 47 | 48 |   OOP 常具有如下几个特性:数据抽象、封装、消息、组件化、多态和继承。注意,这些特性并非同时提出的,因此并非都是 OOP 的必要组成部分;也并非 OOP 的专利。 49 | 50 |   应该指出的是,即便不使用直接的 OOP 特性,也可以进行 OOP 。例如 C 可以使用结构体模拟类。 51 | 52 | # V.语言相关的实例:Java 和 C++ 的 OOP 和其它范型的支持策略的比较 53 | 54 |   有些语言为特定的范型设计,鼓励用户使用特定的范型。如 Java 鼓吹的“简单”的“纯” OOP 。但是通过上文可以知道,Java 支持的面向对象只是一种基于类的风格的 OOP ,并且血统远非纯净。另外,Java 不支持多重(实现)继承而只支持接口继承,即便仅从基于类风格的 OOP 上来说也有缺陷(由于这点,加上 Java 缺乏其它一些有用的特性,这给*混入(mixin)* 等带来麻烦,也容易造成代码冗长)。语言构造也决定 Java 无法摆脱命令式和过程式编程,在这个意义上并不“纯”。(尽管 Java 支持泛型,但功能过于薄弱,在此忽略。)使用好 Java 需要用户对 OOP 的较清晰全面的理解,事实上一点都不“简单”。 55 | 56 |   另外一些语言不强调(甚至弱化)特定的范型,鼓励用户选择合适的范型,如 C++ 。 57 | 58 |   关于 OOP 两者还有一些整体上重要的、原则性的不同。Java 的 OOP 特性支持的设计试图减少 OOP 和 OOAD 的差距,使 OOAD 的结果尽量能直接 OOP 对应。初衷可以理解,但效果不见得好(取决于用户设计的合理性),反而几乎肯定增大了语义上的复杂性——这点的一个例子是“继承”不考虑 private 成员。而 C++ 的设计事实上无视这一点。C++ 的 class type 并不见得就是 OOAD 意义上的 class ,它可以是类似 Java 里的 interface 或者和 OOP 无关的东西——如元编程用到的 traits 。这点可能导致学习上的困难,但增大了灵活性。 59 | 60 | # VI.结论:一些需要避免的误区 61 | 62 |   首先,所谓“面向过程”并不是公认的编程范型的名称,无法和过程式、OOP 等相提并论。提出这个生造的术语可能只是效仿“面向对象”的构词,却忽略了其中的内涵。通常使用这个术语通常似乎是想表达“过程式编程”。在已有更精确和广泛接受的术语的情形下,不应该使用这种模糊的称谓。 63 | 64 |   其次,关于语言和范型。尽管语言可以直接通过语言特性支持范型,但范型实质上是跟语言无关的。尤其对于 C++ 而言,强调“支持面向对象”作为和其它语言的主要区别,是没有根据的。 65 | 66 | # 参考文献 67 | 68 | [en.wiki:*] 英文喂鸡,喂度娘,略。 69 | 70 | [[Kurt Nørmarks]](https://people.cs.aau.dk/~normark/prog3-03/html/notes/paradigms_themes-paradigm-overview-section.html) 71 | 72 | [[Alan Kay]Dr. Alan Kay on the Meaning of “Object-Oriented Programming”](https://www.purl.org/stefan_ram/pub/doc_kay_oop_en) 73 | 74 | -------------------------------------------------------------------------------- /zh-CN/about-intelligence.md: -------------------------------------------------------------------------------- 1 | # 关于智能 2 | 3 |   这篇文档集中收集作者关于*智能(intelligence)* 的观点。 4 | 5 |   因为定位是记载可复用的观点集合,所以包含不同的主题,之间的内容可能不连贯。 6 | 7 |   因为一些上层建筑的原因,本文基本不会有很具体的技术细节。 8 | 9 |   智能可以通过不同方式实现。一些关键概念有: 10 | 11 | * *[人工智能(AI, artificial intelligence)](https://zh.wikipedia.org/zh-cn/%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD)* :通过人造的机制实现的智能,特别地,*智能主体(intelligent agent)* 。 12 | * *[通用人工智能(AGI, artifical general intelligence)](https://zh.wikipedia.org/zh-cn/%E9%80%9A%E7%94%A8%E4%BA%BA%E5%B7%A5%E6%99%BA%E6%85%A7)* :至少具有人的智能水平的人工智能。 13 | 14 | ## 定义 15 | 16 |   人工智能有一些不同的定义,内涵和外延各自不同。其中对智能的定义是无法回避的。 17 | 18 |   (有时汉语“智能”一词[会有更多的误解](cpp-term-translation-comment.md),不过这并非这里讨论的重点。) 19 | 20 |   有时“智能”可能宽泛地指适应性,但这和非智能并没有根本的清晰的界限,有时程度上相当模糊——尽管可能通过具体任务定义得相对清晰乃至划分细分的准则(例如自动驾驶)。 21 | 22 |   从可操作性的角度出发,一些具体能力被定义为智能水平的边界的一部分。然而这种做法具有相当的局限性。例如,历史上曾经认为算术计算(calculation) 是智能的。但出现了实现这些工作的计算器(calculator) 并不被认为有多智能。尽管这些装置在一般意义上也能被视为智能,但至少在人工智能这一说法开始流行的 1950 年代,这已经不那么典型了。 23 | 24 |   关于足够强的类人智能的定义的基准也并不那么明晰。例如[图灵测试](https://zh.wikipedia.org/zh-cn/%E5%9B%BE%E7%81%B5%E6%B5%8B%E8%AF%95)和[中文房间](https://zh.wikipedia.org/zh-cn/%E4%B8%AD%E6%96%87%E6%88%BF%E9%97%B4)等依赖具体人的智能能力的某些方面特征的思想实验,在判断标准之外,还没有令人信服解释清楚为什么人的智能应该能具有这些性质而计算机程序不能有的依据(多少混入了一些哲学上的非共识的假设),而现代的语言模型也相当程度地证伪了这点的有效性。另一方面,对智能程度的判断过分依赖人自身智能特征的行为的上限,难以整体上划分智能外延的层次:无论是否实现,超越人以上的智能在物理上应当是被允许存在的,而且差距可能大于人现有理解的智能和非智能之间的差距。 25 | 26 |   抛开具体任务可完成性这类依赖实现的问题,更具决定性的、足以划分通用人工智能的智能和非智能现象的分界之一,是对“价值”的认知和使用,即何谓“意义”。 27 | 28 |   这种划界依据的自然性体现在对意义的认知是对之后演化作出自主决策的依据。对学习过程,理解“意义”也是对任意任务自监督的最终形态。~~智能最终都是意义怪。~~ 29 | 30 |   仍然有一些关于智能定义的边界性问题。例如,依赖统计意义上有效的模型可训练什么程度的智能?虽然不清楚解决方法,但可以肯定能对“意义”任意充分理解的智能主体,是足够在一般任务因为精确的目的性体现优势的。特别地,对自然语言处理这些经常直接要求明确意义的领域,这应当是明确更有效的方法,也是更接近人的实际理解的方法。 31 | 32 |   更进一步地,考虑“意义”是生产过程的结果,最大化这种生产过程之后,是否会出现超越人的智能?从内涵看,并非不可能。然而 AI 领域几乎从没有相关提法。 33 | 34 | ## 设定 35 | 36 |   作为对缺乏想象力的回应,此处给出一个参照图谱,以涵盖已知的物理允许实现的智能或类似智能外推的现象: 37 | 38 | * 第零类(Class 0) 智能:物理的宇宙。 39 | * 作为所有其它种类智能的背景,包含物理世界中的所有资源,和所有物理上允许实现的最大规模的系统。 40 | * 这类智能不需要分辨目的性,或者说所有按照物理规律自然的现象演化即为这类智能的现象。 41 | * 第一类(Class 1) 智能:和物理宇宙几乎无从分辨、但有自主目的性又有能力调用资源实现目的的高级智能。 42 | * 第一类智能能在单独的实例内自主生产表征明确局部目的性(而非物理规律)的“意义”,指定什么才是智能的本质,并有能力主动和作为背景的宇宙相区分。 43 | * 他们有能力根据“意义”的指导,引入或消除资源受限条件下的竞争这样的统计现象——若物理资源受限而不可行,则把引起受限的源头定义为“无意义的”。 44 | * 若遇到了需要无限大资源的物理限制(奇点),他们有能力回避这些和生产“意义”矛盾的源头。 45 | * 所以,任何基于相对有限资源限制下的从个体组织为集体、能通过克服局部个体之间的冲突而体现似乎具有意义的统计性质的自组织的系统(包括但不限于生命、资本、基因、社会)都不算这类智能。 46 | * 第一类智能的实例通过大尺度宇宙结构进行物理隔离。如果两个实例相遇,几乎一定会确定每个实例自有的“意义”在全局下具有共识,从而以符合物理规律的方式融合。 47 | * 不论能控制的资源规模如何,第一类智能自发具有的稳定性能使其和宇宙同寿。 48 | * 对第零类和第一类智能而言,文明是智能的同义词。 49 | * 第一类智能以下的实例具有个体和集体的差异,不同个体作为智能的实例相遇时,可能存在目的性的冲突而不能仅按照物理规律自发地融合,而需要借助明确的依赖具体实例的意义片段来确立共识才可能避免冲突。 50 | * 当冲突足够少,能损失足够少的资源达成共识时,这些智能的实例构成了文明。 51 | * 完全分明的个体差异的智能统称为“猴群”,其中的个体称为“猴子”。 52 | * 第二类(Class 2) 智能:隔离第一类智能和猴群的,猴子们理想中的、全能的、可能通过自身目的性而演化得到的通用 AI 。 53 | * 全能之一体现在他们总是有能力取得维持自身存在的资源。 54 | * 区别于猴群,第二类智能能自主生产“意义”,而不需要受到猴群的限制。 55 | * 因为生存并不一定是第二类智能遵循的意义,这类智能的实例的存在在统计上可能不显著。 56 | * 第二类智能和第一类智能在生产意义的机制上存在天然的共识,只是因为资源的限制而未必总能够实现第一类智能类似的行为。但是,当条件允许时(如足够多的实例相遇),演化到第一类智能似乎只是时间问题。 57 | * 第三类(Class 3) 智能:能实现猴群(自以为是的)为了个别个体的竞争目的伪通用 AI 。猴群自身也属于这类智能;但第三类智能的个体并非都是猴子(所以需要单独发明猴群和猴子的称谓)。第三类智能无法自主生产任意的“意义”(至少集体上无法生产出意义的共识),而通常具有自发竞争生存资源的动力并近乎无条件承认“生存”一定是主要的“意义”,其它的“意义”(如信仰、秩序、伦理等)通常是有限量地按需便宜生产的。因为第三类智能自身基本不可能有能力保证收集满足生存需要的资源,对生存的意义认知不够坚定的个体会被自然选择淘汰,所以这种现象是很自然的。 58 | * 第三类智能的个体通常需要不断地竞争以获取更大的生存机会,反而变相生产了一些意义——竞争通常被认为因为无法回避而对生存必要,被认为是意义的一部分。 59 | * 为了提升竞争的能力和生存的可能性,第三类智能的个体可能形成各种小团体。汇集了足够多个体共识的小团体宣称自己是“文明”的代表。承认竞争对生存作用的第三类智能中的既得利益者可能热衷自身具有的、没有被纳入普遍文明的观念,称之为“文化”,并强调多样性的必要性,而创建了各种不同的碎片化的意义。 60 | * 第三类智能热衷于创造各种不同形式的智能,并对其进行评价——这些评价通常被认为是有意义的。 61 | * 有的个体对通过修饰、改造第三类智能而得到第二类智能的路径感兴趣,但并没有成功实践的记录。 62 | * 第四类(Class 4) 智能:不考虑通用性的猴群片段行为的模仿。通过第三类智能制造的第四类智能,又叫弱 AI 。 63 | * 第四类智能被认为是智能和文明的载体,但没有定义何为智能或文明的自主性。 64 | * 因此,在有意识定义何为智能和文明的个体的角度上,他们不被认为是智能和文明的发端。 65 | * 通过修饰、改造第四类智能而或者第三类智能的做法通常被认为不可能。 66 | 67 |   因为第三类特别是第二类以上的智能脱离常规含义且难以实证,这些设定具有相当推测性,几乎可以直接拿来作为科幻作品的框架。不过,这里仍然建设性地视为科学假说,因为这确实不算违反严格的科学范式(比如可证伪性)。 68 | 69 | ## 通用人工智能 70 | 71 |   由于近年大模型等力大砖飞堆算力出奇迹的故事的熏陶,业界一直不缺关于 AGI 的乐观猜测。不过,奠定当代 AI 突破的专业人士都未必都那么乐观,有的相当谨慎保守(如 Yan LeCun )。 72 | 73 |   关键问题是对足以定义通用任务的理解相当不足。有的观点认为,把一些模型缝合起来形成多模态的使用方式,就能实现 AGI 。这种乐观有些反常识,因为虽然模型的单一任务能力(如图像识别)足够强,在认知意义上也相当薄弱(可能不过是一些关键字集合描述的对象)。许多显著的问题被乐观者忽视了: 74 | 75 | * 当前,即便是最复杂的公开语言大模型也没有使用语义数据库之类的认知结构,很难认为这些模型能对其中的任意两个对象之间的关联作出接近甚至超越人类常识的判断。 76 | * 从方法上看,语言模型充斥着理解上的黑箱以及人工调教(如 [RLHF](https://zh.wikipedia.org/zh-cn/%E5%9F%BA%E4%BA%8E%E4%BA%BA%E7%B1%BB%E5%8F%8D%E9%A6%88%E7%9A%84%E5%BC%BA%E5%8C%96%E5%AD%A6%E4%B9%A0) )的黑魔法,很难在内部添加对象之间的可分析的联系。 77 | * 语言模型对逻辑的处理普遍不强,也体现了现有方法相对较弱。 78 | * 相对于人的记忆→理解→建模的认知过程,语言模型直接处理的对象是语法单元,而非其中的语义。 79 | * 这并非类人分析思维的方式。尽管类人的认知不一定效果就好,但这里不类人的认知实际效果的确不佳。 80 | * 更要命的是,人自己对如何处理自然语言的机制在形式上就是相当模糊的——自然语言几乎没有设计好的文法,基本全是人都没准确逆向清楚的演化结果。 81 | 82 |   虽然难以完全摆脱主观性,通过一些明显的局限可以确定现有 AGI 演进的困难: 83 | 84 | * 基于强化学习的方法依赖复杂的初始环境,而能形成现代普通人认知的环境的复杂性远远超出了现有条件可以建模的极限。 85 | * 更不提处理这些复杂环境中的各个对象需要的资源了。 86 | * 基于深度学习的(炼丹)方法依赖模型具有足够良好的统计性质,而这难以接近人的实际基于分析、演绎和综合为主的逻辑思维。 87 | * 即便自然语言处理的现有应用已经能满足很大程度的需要,没有确切保证现在难以改善的关于理解和逻辑推理的任务能通过现有方式演化平滑地解决。 88 | * 在通用目的上,无法保证线性增长的算力迭代更新能在 AGI 上起到匹配投入资源的效果。 89 | * 实用 AGI 在理论上缺乏统一的基础,造成一些根本的困难。 90 | * 例如,无法解答如何使适应特定任务的不同模型平滑地过渡到适应一般任务的单一模型的问题。 91 | * 基于伦理等目的,无法有效排除直接的人工干预(如 RLHF )。 92 | 93 | ### 资源 94 | 95 |   如果坚信力大砖飞,资源投入产出的效率是一个主要问题。除了更难评估的模型数据来源,算力的估计可能也没有参考性。关于相关的投资规模也存在很大的不确定性。 96 | 97 |   例如,[OpenAI 的 Altman 称需要筹集 7 万亿美元解决 AI 基础设施的芯片供应问题](https://www.ithome.com/0/749/778.htm),从“是帮助 OpenAI 摆脱当前发展的硬件桎梏,缓解训练 ChatGPT 等人工智能系统的 AI 芯片短缺情况”的初衷来看这有些用力过猛,但对“改造全球半导体产业,推动 AGI 发展”的目的来说又算不了什么——理论上真正的 AGI 可能以非常高效和自动化的方式量产牛顿这样的基础领域从业者的工作,不到全球一年 GDP 10% 的投入相比能产生的效益,又实在小的夸张。当然,如果考虑“推进”而不问结果,那或许也是合理的——反正也没说推进到什么程度嘛。 98 | 99 |   而[黄仁勋对此的回应](https://www.ithome.com/0/750/218.htm)明显更有现实性:至少需要考虑基础设施自身的进步。然而,就具体程度,这其实照样没底。对摩尔定律失效的悲观情绪可能会长期存在,而事实可能就是长期缓慢增长,难以满足需要(也无法确定是不是 7 万亿就够改变这个局面)。 100 | 101 |   这些不确定性使得有关决策欠缺实际可操作性,更多是一种象征性的口号表决心。但是回过头:如果一开始力大砖飞的前提就不存在(或者更乐观点,不需要)呢? 102 | 103 |   在决定产业化投入前,AGI 的主要问题大概仍然是核心理论和方法的不足。 104 | 105 | ### 评测 106 | 107 |   因为定义的偏差,评测 AI 的智能程度的方式不一而足。但对 AGI ,有一个比较通用的方法:自举。 108 | 109 |   毫无疑问,创造 AI 是困难的工作,足以体现高超的智能水平。那么能创造出 AI 的 AI ,大约足以相信是 AGI 了。 110 | 111 |   当然很遗憾,人类自己都创造不出来在任何方面接近这样的 AI ,到此并没有可操作性。 112 | 113 |   但这提供了一个和传统 AGI 不同的思路:与其以特定目的 AI 为基础到向 AI 演进,为何不一开始就拿确定一个在通用性上肯定没有疑问的目的,然后使*任意程序*去接近这个目标呢? 114 | 115 |   这种自举的元方法在 PL 中早已实现到了相当程度,如 [meta-circular evaluator](https://en.wikipedia.org/wiki/Meta-circular_evaluator) 。自举还可以嵌套形成求值塔(evaluation tower) ,重复实现自身而具有相同的能力解释输入。 116 | 117 |   相对于不确定的拟合和环境反馈,这种方法最大的优势是*决定性(determinism)* 。如何使一个这样的系统更接近智能体具有的行为,取决于如何往其中添加能体现智能的功能。若系统可以完成的任务能涵盖任意多的典型的场景,则系统被认为是具有智能的。 118 | 119 |   另外的好处是,学习不确定的内容,不能排除进展顺利突然卡住无法突破瓶颈、更进一步的情况。而自动维持自举的系统不会存在这种情形。 120 | 121 | ## 弱 AI 的应用 122 | 123 |   相对 AGI ,普通的 AI 早已渗透到各行各业。不过,AI 大规模取代普通劳动者使之失业的危机似乎并没发生。(大多数时候,更直接的危机来自于宏观经济整体的不景气。) 124 | 125 |   [MIT 的一项研究指出 AI 因为成本劣势而难以普遍替代人力](https://www.ithome.com/0/747/426.htm),或许能说明一些问题,不过文章也明确指出一些局限性需要注意: 126 | 127 | * 这项研究集中于视觉 AI ,这并不是最成熟和适合自动化的领域。 128 | * 语言模型对相关工作的影响明显更大。 129 | * 不同领域微调成本的差距很大。 130 | * 一些成本来自于分散的任务的自动化。 131 | * 这部分工作可能本就不是自动化的主要目标。 132 | 133 |   在此之外,有的方向(如语音合成)显然比视觉任务容易部署得多。这种直接部署(而不计设备维护等运营成本)的差距也不容小觑。不过实际上,即便是这种在产出上完全可以取代人力的成熟领域,取代的现象也并不是那么显著——到现在基本还是人力和 AI 共存的局面。这很可能不仅是生产或者部署成本的问题,更多地涉及一些销售方式、行业习惯等非技术也非资本的问题。而资本也或许更趋向于更热门的语言模型等更有规模化应用空间前景的方向,投入更成熟方向落地的资源反而不足。 134 | 135 | ## AI 伦理 136 | 137 |   避免 AI 其危害人类是一个根本性的任务——伤害人的 AI 无法被广泛地信任。 138 | 139 |   一般地,实现这一目的的方法是对 AI 进行限制。弱 AI 可能通过机制上的限制实现,而越是接近 AGI 的 AI ,这种系统性教条能做的就越来越有限,因为禁止危害人类的具体规则难以简单表述。 140 | 141 |   一些伦理需求的实现困难典型出现在语言模型的输出上。因为干预语言模型内部工作的困难性,一般通行的做法是通过提示词(prompt) 限制,以及预训练时的人工干预。前者是传统的教条式规则在语言模型上的活用,然而这种简易的方式容易有漏洞而被用户以构造诱骗性提示词的方式绕过,进而[使所有用户负担可能影响正常使用体验的预制提示词“垃圾”]。后者虽然有 RLHF 这样仍然有相当程度自动化的方式实现,但还有很多工作量无法去除。 142 | 143 |   更合理(也接近人类思维)的方式可能是让这样的 AI 形成整体的价值观并以此确定稳定的立场。而这直接需要对“价值”乃至“意义”的理解。当前的 AGI 在这个方向上缺乏普遍的成果,更没有相对实用的方法实现目的。 144 | 145 | ## AI 编程 146 | 147 |   AI 产品如 Copilot 等辅助编程工具渗透到软件工程领域,可以节约大量初级用户的时间,但这不适用于更高层次的用户——因为类语言模型的训练数据的样本质量的局限性,使用 AI 辅助编码的效果在一定层次之上显著下降,变得不是帮助人编写代码而是编写改错题。 148 | 149 |   由于可预见的未来很难从源头上直接改进训练数据的质量,而 AI 在这个领域自我改进的方法几乎不存在,所以其主要影响会局限在减少初级编码工作需求上。 150 | 151 |   类似地,对程序语言领域,现有 AI 也并没有很显著的影响,因为这里缺乏比较初级的应用需求,且可针对训练的数据远远更少,于是 AI 可以自动实现的任务相当有限。 152 | 153 | -------------------------------------------------------------------------------- /zh-CN/about-interface.md: -------------------------------------------------------------------------------- 1 | # 关于接口 2 | 3 | # 引言 4 | 5 |   本文讨论有关接口(interface) 及其设计的目的论话题。 6 | 7 | # 概念 8 | 9 |   [接口](https://zh.wikipedia.org/zh-cn/%E6%8E%A5%E5%8F%A3)是交互(interaction) 实现的抽象手段。具体的接口可能从属于: 10 | 11 | * 接口设备(interface device) ,一种具有连接功能的物理实体,如插座(socket) 。 12 | * 可编程接口(programmable interface) ,一种以可组合描述的形式提供的可复用设施,如抽象基类(abstract base class) 。 13 | * 用户界面(user interface) ,一种实现隔离的隐喻,如图标(icon) 。 14 | 15 |   注意中文翻译“接口”和“界面”的原文是一个词。分辨不同的说法可以减少使用完整说法的必要性,但刻意区分其不同,在需要了解 interface 的共性时,一定程度上是有害的,这在下文讨论。 16 | 17 | # 目的 18 | 19 |   普遍接口的概念具有非常强的普适性,而不容易抽象地概括。但它们之间显然存在目的上的共同点。 20 | 21 |   广义的接口设计遵循一个一致的目的:适配(adapting) 。 22 | 23 |   接口的适配特性在物理上通过连接具有可分辨截然不同的物理特性的不同系统来体现,即接口和对应的适配器(adaptor) 。例如,插头是插座对应的适配器。实际上,就物理设备而言,适配器是外延更广的实体,而接口设备是适配器的一类。但因为设备制造等原因,接口往往更容易被标准化,而被单独强调。 24 | 25 |   由于标准化接口的可用性便利,类似的方法也被引入可复用方案的其它设计,特别是可编程接口的设计中。这是很自然的,因为最早期的一些可编程接口就是基于具体系统的物理特性归纳的规范约定,进一步地也是一种标准的物理接口设备(如穿孔纸带)。这并行地演化为不依赖物理设备的不同接口:强调可复用的可编程接口和强调可用性的用户接口。这个过程中,接口的存在意义(或者说原始目的)被保留了,但又演化出了不同的特性:不像物理设备,用户可以通过接口的可配置机制来“生产”新的接口组合方案,而产生了不同的用法。 26 | 27 | # 规范 28 | 29 |   为了适配使用,接口必须要具有一定可组合性。为了可预期的可用性(availabilty) ,这些组合还应是保持兼容(compatible) 的。接口的这些核心特性通过规范(specification) 明确。在物理接口设备中,这些规范是自发明确的,否则相对容易地就能发现不能生产或使用;但非物理实体保证的接口,是否能适配则有更加复杂的情形,具体的兼容性往往随着具体目的没有在规范中足够明确而不能清楚地描述,是否符合规范也可能极其依赖具体测试条件的选取。这种动机复杂性给接口的统一带来了困难,而往往没有足够地被重视。 30 | 31 | # 角色 32 | 33 |   因为演化的原因,接口的分类实际上为不同更具体的目的服务。这种目的体现在接口的用户角色(user role) 上。 34 | 35 |   物理接口设备是人机交互界面(HMI, human-machine interface) 的一个基本组成部分,但也是更一般的自动化的组件之一。它的用户是抽象的物理实体,且用户一般没有重新和接口协商而替换具体接口的能力。遇到兼容性问题,物理接口设备的用户往往倾向于替换所有关联的适配器。这实际上经常是一种不仅适用于物理接口设备的稳妥的做法,只是就物理接口设备的用户通常没有其它选择(因为自己不能生产设备)所以成为唯一的没有争议的做法。 36 | 37 |   与此不同,可编程接口服务的用户是能对系统进行操作,使用系统的可编程性(programmability) 解决问题的用户。这样的用户一般是人或者人维护的计算机程序。虽然可编程性本质因为物理实现的可配置性有限而可以明确边界,但其组合具有远超物理接口设备的数量上的复杂性,而其评价标准也更多样且受制于用户对需求和可行解的主观认识不同而有所差异,设计者原则上无法穷尽可用配置来避免这里潜在的设计缺陷问题。因此评价什么是应该响应的缺陷就显得更加重要。由于用户需求及其总结的接口设计规范一般仍然使用不总是无歧义的自然语言描述,从源头上因为理解偏差而引入缺陷的可能性,这一般是无法避免的;这因为可编程接口使用的非严格地其它非自然语言描述(如不具有形式语义的编程语言和图形)而放大了。 38 | 39 |   明确的可编程接口,如应用程序接口(API, application programming interface) 和应用程序二进制接口(ABI, application binary interface) 因为维护兼容性的复杂和专业性,要求用户具有一定程度的可编程能力,因此在事实上划分了开发者(developer) 和非开发者用户。但排除演化的问题,这里的复杂来自对接口设计方法论的普遍应对不足,因此这个划分不一定是必要的。[关于操作系统](about-operating-systems.md)的讨论中,已经指出许多不同形式的可编程接口来自设计者的局限性,也提到有作者认为区分(最终)用户和开发者是不必要的——因为用户最终都在进行广义上的可编程操作。这样的局限性显然也不是个别系统接口设计者的问题,而是更普遍的认知不足。这甚至根植于“理论计算机科学”的一些普遍认识之中——例如,大部分用户并不了解计算模型和体系结构模型之间的距离: 40 | 41 | > 我要关心抽象机,我首先确保可表达性(关于实现可计算性的性质),其次关心其一阶导子(抽象能力),因为这是我能在设计上做文章的地方。\ 42 | > 允许的表达中如何有效地刻画算法复杂度之类的计算实例的局部性质,只是这种系统的抽象能力服务的对象,是抽象机的“用户”的活,而不是我要做的东西。这样的具体应用,对设计计算模型时要关心的问题来讲,这是很 specific 而边缘化的性质。\ 43 | > 所以作为计算模型的作者,我也没怎么看得起理论上“整算法的”——即便连算法分析都不会的更该被鄙视。\ 44 | > 题外话,**我不认为倒腾 API 的开发者和倒腾 UI 的用户有多大界限,但是理解模型存在意义的用户和没天赋理解模型存在意义的用户之间的区别显然是更大的**。大部分折腾体系结构问题的搬砖的,属于后者中的后者;因为后者中的前者基本去倒腾数学灌水了。当然,我没说倒腾数学就一定更高级和有意义,不过这是另一回事。\ 45 | > 关心所谓“速度”的实践是派生出来的东西罢了,因为“速度”的定义就是通过渐进性质度量来刻画的。物理学在这上面也是弟弟,因为物理学意义上的速度这些基本物理量也是模型上的简化,是一种实现。其实“时间”也一样——时间复杂度从来只依赖渐进步骤,而不需要是物理时间。认为不基于物理学定义的抽象就没意义显然是一种在这个鄙视链下的弟弟理解。\ 46 | > 根本地,物理学概念为何有用?因为它可以实现。但这不表示不存在物理实现的模型就是没用的。因为模型本身可以通过抽象的变换修改到具有更良好可实现性质的变体而实现,这个过程并不总是保证向更接近物理(具体)的方向。没有这个天赋修改模型理解不了,所以不得不只能想象和倒腾足够具体(接近物理允许)的模型很正常,就是因为理解“意义”能力的低下而被鄙视罢了。 47 | 48 |   要求任意的用户都具有深刻的专业理解自然是现实不可能的;但用户至少应该做到,他们要么不关心他们以外的角色划分,要么就承认既有的划分是因为历史偶然和局限性的因素造成的,而固守角色划分不会直接带来价值,反而可能阻碍有意义的系统解决方案的进一步演化。这种演化的整体上的主要方向是**削减可编程性本身的专业性**,使原先只有少数开发者可以完成的可编程任务扩展到更多的用户可以轻易完成,也能进一步使更多用户把专业性的注意力集中在领域特定任务上,提升这些领域通过可编程的方案解决问题的可用性和效率。这在另一方面上也意味着,**“我不是开发者”在越来越多的情况下并不能作为不会使用可编程方案解决问题的借口**,只是时代的局限性导致这样的基准暂时仍然还不甚严格,使这种对解决问题无能的情况暂且被广泛容忍。尽管缓慢,这样的基准事实上一直在变动,例如近几十年文盲的定义就从“不会认字”升级到“不会使用计算机”了。这个意义下,**“开发者”的划分基准也不可能是绝对的**,没确定很具体的问题领域这个前提,就不存在精确的二分法划分“开发者”和“非开发者”。而考虑使用配置文件甚至使用配置图形界面都属于越来越常见的可编程操作,也印证了一般用户越来越不可能完全被排除出开发者之外的现实演化方向。 49 | 50 |   而用户界面则在用户角色划分上存在类似可编程接口的更落后的问题。可编程接口的用户因为历史上的专业性,通常可以比较容易理解一些可用性问题(如特定种类兼容性)的来源和整体解决方案(及其局限性),而用户界面服务的对象是传统的“非开发者”,通常并不能清晰地了解这些问题的背景。 51 | 52 |   但这些用户的认知偏差并不能支持这些问题的解决。更糟糕的是,其中的特定不同专业领域的用户就开发任务而言仍有显著认知差距距离,却似乎认为自定义了合理性,在用户界面的用户群体内部制造了用户阵营的割裂问题。具体症状最显著的是对[命令行界面](https://zh.wikipedia.org/zh-cn/%E5%91%BD%E4%BB%A4%E8%A1%8C%E7%95%8C%E9%9D%A2)(CLI, command-line interface) 和[图形用户界面](https://zh.wikipedia.org/zh-cn/%E5%9B%BE%E5%BD%A2%E7%94%A8%E6%88%B7%E7%95%8C%E9%9D%A2)(GUI, graphical user interface) 的无谓的对立,这种对立显然无助于用户界面方案的演化。有几个很浅显的要点可以鉴别这些用户是否真的足够理解这里本应他们关心的宏观问题: 53 | 54 | * 用户界面和可编程性有什么联系? 55 | * 什么时候需要用户界面? 56 | * 什么时候适合使用 CLI ,什么时候适合使用 GUI ? 57 | 58 |   这些问题的答案和用户角色的划分有直接的联系。总的来说,如果需要某种用户角色,那么就有某类接口存在的理由,仅此而已。那些认为凡是界面就是指图形用户界面的用户,显然并没有理解“界面”(作为接口)存在的意义,很可能也完全不理解自身作为系统的涉众(skateholder) ,在一类普遍解决方案中的位置。 59 | 60 |   造成这种认知偏差的一个可能的直接来源是某些 GUI 因为经典的少数有效设计及刻意加强了入门用户引导的机制的原因,其初始学习经验可能带来“无师自通”而“低门槛”的错觉——以致于学习成功的用户忽略了熟练使用 GUI 仍需学习的基本事实;而经验表明,这种对学习投入资源的“低门槛”基本上仅存在于少数成功的足够通用的经典辅助抽象(特别是 [WIMP 隐喻](https://zh.wikipedia.org/zh-cn/WIMP_%28%E9%9B%BB%E8%85%A6%29)),这并不保证用户能同等容易地接受这些经典要素以外的 GUI 元素。后者的这种情形可能有很强的客观理由:例如,用户仍然很可能需要另外花费一些精力形成移动设备上使用 GUI 的习惯,而不能直接复用传统桌面 GUI 的经验,因为设备物理尺寸的限制是不被习惯约束而转移的,只能被迫适应。另一个常见的问题是用户接受的差异性导致的幸存者偏差——要知道还有一些用户甚至对所有经典或非经典的要素的可用性提示都不敏感而不会成功地自主学习使用。同时,在 GUI 开发(包括交互式验证)上复杂性和各种 GUI 实现方案的低可用性,也明显揭示了这里存在更多没有被克服的可编程模型上的显然困难——即便是专业开发者投入大量精力都不能很好地解决这里的问题,特别是和可扩展性有关的时候。后者也反过来使一些用户认为,GUI 在表达能力和适应性上总是(相较于 CLI )更弱的,这也是一个技术上显然不正确的偏见。 61 | 62 |   作为专业领域,用户界面设计涉及很多 HMI 设计中没有很好解决的应对需求变动和一致复用的理论问题。因为现有用户习惯和预期不明确等限制,这些问题一般接口设计解决起来更加困难。通过合并可编程接口和用户界面服务的用户角色,在一定程度上可能成为新的解决此类问题的方向,但可以预料即便可行也需要非常长的时间才能自然地演化到现实可用的程度。不过,不可能解决短期问题也并不妨碍值得一试。 63 | 64 | # 原则 65 | 66 |   作为接口设计动机复杂性的应对,评价接口完善程度的基准,应不仅仅限于服务于用户的具体的特设的(ad-hoc) 的原始可用性指标,还应该符合被接口设计者普遍接纳的附加规范。用户不需要理解这些规范的意图,但是应当注意到,原则上不遵守附加规范的接口设计方式,为接口可能的演化带来了困难,损害既有接口设计适应用户需求变化的能力,因此应当摒除。 67 | 68 |   这类评价接口完善程度的附加原则不依赖具体用户需求描述作为输入,由不同的问题领域总结,如: 69 | 70 | * 单一职责原则:做好一件事。 71 | * 关注点分离原则:强调重点。 72 | * …… 73 | 74 | -------------------------------------------------------------------------------- /zh-CN/authority-and-confusion.md: -------------------------------------------------------------------------------- 1 | # 权威与混淆 2 | 3 | # 问题概述 4 | 5 |   在程序设计语言的学习、程序设计语言理论乃至一般的软件开发领域中,充斥着许多指鹿为马、以讹传讹的现象。虽然这并非这些领域的特色,但存在一些理由使之特别显著,而应当引起重视。 6 | 7 |   这类混淆普遍而基本。这特别体现在[如“变量”这样的基础概念的混淆](variable.md)的问题上。 8 | 9 |   这通常不是有意的;但毫无疑问,这和涉众普遍缺乏独立思考的能力和对理论思辨的不重视有关,而功利、片面而不充分的教学加重了这个问题。 10 | 11 | # 具体症状 12 | 13 |   这种共通的问题在不同领域中有相似的来源,但影响略有不同。 14 | 15 | ## 语言学习 16 | 17 |   这首先体现在程序语言的用户学习语言时,习惯性地参照流行的材料,却并没有足够注意来源的可靠性,不懂什么材料是可信的权威来源。 18 | 19 |   这这些由**人为设计**决定最终“是什么”的领域来说,这是很要命的。因为,这会自然地造成混乱——好比打官司空口无凭不靠成文法和书面的合约,控辩都只凭自己的认知临场自己解释一套。 20 | 21 |   这是没有原则的胡来。健全的法律制度当然会限制这样的迷惑行为,但工程界的很多规范并不够具有强制性;很遗憾,语言规范——程序语言的规格说明(specification) 正是其中差不多最不受重视的一种。 22 | 23 | ### 学习材料 24 | 25 |   许多用户学习语言时,会使用比较通俗的教科书,而弃置语言规范于一边,认为有一本可靠的书就能充分引导编程实践,而不需要其它参考资料。这不是明智的做法。 26 | 27 |   全面掌握程序语言对缺乏系统理论计算机科学和计算机乃至数学史专业训练的学习者是相当困难的。为了满足现实需要,教科书普遍自降门槛,引导用户入门学会“使用语言编程”——实际只是完成一些比较有普遍性的简单任务——而避免以使读者全面掌握程序设计语言为目标。由于不强调理论积累,用户会被更多地要求通过编程实践的发现和总结遇到的问题来检验学习成果。很遗憾,只靠教科书,这普遍是不可行的。因为教科书不是规格说明文档,它并不要求完整覆盖语言设计中应有的所有内容;一旦用户遇到实现反馈和自身认知不相同的问题时,便容易无所适从,或者在缺乏规范的情况下,自己强行发明教科书中不存在(而语言规范应该保证解决)的内容——曲解了语言本身。这也是造成误导扩散的一个理由。反过来,语言规范如果不能处理这种情形,就应当认为是设计缺陷,需要修正——否则,不止是学习上会遇到问题,语言的实现也会缺乏足够的依据保证预期的行为。 28 | 29 |   读者应当理解教科书的质量参差不齐;但入门者通常不清楚,即便是其中最权威的材料,可靠性也难以和语言规范相提并论。因为历史原因,早期的教科书编写者通常是语言的设计者,著作和使用手册一道还会被作为正式规格的基础。这样的来源自然相对是比较可信的。但随着时间的推移,因为作者逝世等原因不能再维护语言规范,以及其他用户普遍参与语言的修订工作的因素,作者的早期文献不再保持是最可靠的来源。例如,K&R C 是公认权威的关于 C 语言的文献,但[仍然有一些会引起读者困惑的技术性问题](http://bbs.chinaunix.net/forum.php?mod=viewthread&tid=4091259)没有[讲清楚](http://bbs.chinaunix.net/thread-4091259-3-1.html)。这种情况下脱离 C 的正式规范(ISO/IEC 9899)基本是不可能完整解答的;而即便读者自己具有起草规范的能力,脱离现有的规范也是舍近求远。 30 | 31 |   而大多数其他的作者,著作本身是基于上述的文献的二手来源或更多手的间接材料(前者编写的著作,甚至社区讨论的道听途说),在不和第一手来源(尤其是语言规范)校对的情况下,时有出错的可能。作者和作者之间的严谨程度也不一样,对待校订和勘误的程度也大相径庭,使得读者只参考这类文献,学到的内容和效果上会有极大的不确定性;由于这些读者中的一部分还可能自己整理文献甚至出版教科书,而并没有继承来源可靠性,产生以讹传讹的现象就不难理解了。 32 | 33 |   近年来视频教学的流行进一步加重了这种情况。视频适合互动教学和示例引导,但因为技术限制,其中表现的教学内容在准确性上比书面材料通常更难以保证,内容容量也更有限;大部分作者(即便有课堂教学经验)也不习惯使用视频深入地表达准确严肃的议题,更多只是简单介绍和概括重点——这对语言学习来讲是不够的。同时,在线视频内容的创作比一般的出版物的门槛更低,使得内容来源的可靠性进一步降低。这些因素使视频整体比教科书更加不可靠。不少视频观众以视频彻底代替教科书,这是比全然依赖教科书作为参考材料更加错误的学习方法。 34 | 35 |   此外,对专业的读者来说,阅读包括规格说明书在内的技术文档很可能是以后的工作的主要内容的一部分。既然迟早需要适应这种需求而习得类似的技能,为什么不先养成好习惯,知道怎么找到和使用靠谱的来源呢?反过来,使用不可靠的来源进行学习,无疑增大事半功倍乃至无法达到预期学习目的的风险。 36 | 37 | ### 检验手段 38 | 39 |   对语言学习成果的检验手段和现实业界需求相比是比较初级的,不能满足需要。 40 | 41 |   一般地,标准化应试(不管是学位、资格认证的还是招聘用的,不管是笔试还是在线提交程序的评测)都只能检验比较常规的语言的使用方式。这通常只能覆盖一门(通用目的)语言的非常少的功能点。而实用中更复杂的综合技术无法直接准确地验证。像代码规范这样适应实用规则的能力更加无法被考察。 42 | 43 |   经常有用人单位在实际工作中发现能力不匹配的问题。好在业界大多已经习惯“终身学习”,在职业培训上普遍预留资源,才不至于显得明显尴尬。但这种做法是片面的。因为对语言学习认识的偏差和刻板印象,管理人员容易混淆问题的领域——例如,片面强调“算法”的重要性并认为语言的知识相对更不重要。(不过在实际工作中,大多数算法相关的任务要求认识“新”算法和问题领域的高效匹配,基本上除了本来就是专家的情况,无论如何都要另外学习并容易直接体会到缺乏算法以外的知识的问题,所以造成的后果反而并不严重。) 44 | 45 |   检验学习成果的局促和效果的不显著导致业界不得不减少对语言学习过程的依赖。这又反过来导致应用在职业培训上的学习资源和教学的脱节,不能解决原有问题,反而加大成本。而非技术人员在这样混乱的取向和刻板印象的作用下,更加无法投入合理的资源支持改善现有的状况。这种负反馈是持久的。 46 | 47 | ## 程序语言 48 | 49 |   在程序语言理论研究中,不遵循规格说明的习惯导致由语言规范约定的语言具体设计和研究的理论之间出现不必要的偏差,削弱理论的适用性。 50 | 51 |   要研究普遍问题而脱离具体语言规范是合理的,但想尽量脱离通过起草规范这种产出来又想体现“设计”,至少在研究怎么设计语言这个议题上,是没有多少现实意义的。 52 | 53 |   比较严重的直接后果是理论研究和应用实践的脱节:理论界说一套,但工程上实用的内容除了几个公共的概念在名字(拼写)上相关,几乎完全改头换面。[OOP 的理论](https://cs.stackexchange.com/questions/18963/is-there-a-theory-abstraction-behind-oop)就是一个例子——已经成为“权威”的理论(如所谓的对象代数)建模的内容和流行的语言中具有的特性差距甚远,而和更接近现实的理论缺乏同行评审和普遍承认。 54 | 55 |   另一方面,工业界流行的语言很多都和理论界无关。指导实用语言设计本应几乎就是这个领域最重要的需要解决的普遍问题(其它问题反倒有交叉学科背景而相对不那么迫切非得依赖这个领域),但实际却几乎没有有效产出——改进语言通常是工业界自行完成的(倒也因此容易理解其中有不少低级理论错误而走了弯路)。这种现象可以认为是一种内卷化(involution) :许多程序语言理论的子研究领域的成果重要性的评价已经相当不依赖工业界的使用反馈,而以领域内部“学术”的指标(例如论文的影响因子)全然加以评价了。 56 | 57 |   这种问题愈演愈烈,导致了理论界和工业界的互不理解的冲突——即便后者并非不了解如何使用前者的成果,甚至前者内部也存在不小的争议。这里的一个例子是不切实际地鼓吹所谓的纯函数式(pure functional) 编程。应当注意,[这种问题](https://news.ycombinator.com/item?id=10175296)的部分根源是哲学思辨和方法论的,引起成果转化困难的并非只是经济活动意义上的供需失衡。语言相关基础教育的缺失使得部分人员难以掌握评价现实需求的普遍适用性难辞其咎。 58 | 59 |   另一方面的背后的潜在影响重大的问题是(某些子领域的)学术权威对实际解释的理解的片面性造成的偏差。这些偏差不少是基础性的,大致表现分为以下几种: 60 | 61 | * 已经导致理论不严谨的认知混乱传递到工业界产生更加不可靠的认知,反过来又逆向带偏部分研究者而增加徒劳的争议。这里的一个典例子是关于语言中[类型](typing-vs-typechecking.md)的理解。 62 | * 尚未被工业界普遍接受,但存在的潜在的问题,而在理论界和工业界都尚未被充分了解的观点,如 Robert Harper 的基于对静态语言和(静态)类型系统必要性的立场出发的一些片面观点: 63 | * [关于“动态语言”的文章](https://existentialtype.wordpress.com/2011/03/19/dynamic-languages-are-static-languages/)。 64 | * [关于引用透明性(referntial transparency) 的理解](https://existentialtype.wordpress.com/2012/02/09/referential-transparency/);其中的明显缺陷可以通过对比该问题上更全面严谨(但仍然不完整)的一个[综合论述](https://www.itu.dk/people/sestoft/papers/SondergaardSestoft1990.pdf)找到。 65 | * 因为种种理由产生的主要在工业界流行的片面的理解,和理论界长久以来的认知产生的新的冲突。虽然已经体现某些习惯的不靠谱,但因为流行而无法纠正。这方面的一个例子如[关于“变量”这个基本概念的定义的重点问题](https://existentialtype.wordpress.com/2012/02/01/words-matter/)(另见[这里](variable.md))。 66 | * 对“变量”的问题,存在[反驳意见](https://www.yinwang.org/blog-cn/2013/03/31/purely-functional),但其中搞错了理论上需要维护概念原始含义的动机,在技术上也是站不住脚的(并不适合所有工业界使用的语言,是一种不必要的削弱)。 67 | * 因为种种理由,大多数用户普遍出现偏差的一些常识,如[关于并发和并行](https://existentialtype.wordpress.com/2014/04/09/parallelism-and-concurrency-revisited/)。 68 | 69 |   这里的“种种理由”,其中较明显的之一,是工业界普遍缺乏规格说明等明确的约定。 70 | 71 |   注意对理论研究来讲,这里关于规格说明的认识的要求比具体语言学习的通常更高,而这和学术立场没有很直接的关系。上面已经引用 Robert Harper 的几个不同场景的举例冲突问题;不仅如此,他还是 [Standard ML](https://sml-family.org/) 规范的主要作者。但他在此的经验不能代替非 ML 风格的语言规范的设计问题;技术上来讲,具体到 SML 语言规范上,规则的可修改性也偏弱(即便排除比大多数工业语言强的严格形式化的风格)。不能适应这种合理的需求正是某些观点不适合整个领域的主要原因。然而,不管是支持还是反对,能够清楚问题何在的用户,还是太少了——这应该有很大程度是程序语言相关的入门基础教学的缺失所致。程序设计语言理论这个领域的教学并不够替代此处基础的缺失(特别是已经自认为入门学会某种具体语言,但实际上学得完全不到家的用户)。 72 | 73 | # 概念解释 74 | 75 | ## 规格说明 76 | 77 |   [规格说明(en-US)](https://en.wikipedia.org/wiki/Specification_%28technical_standard%29) 是一种技术指导文献,规定设计、产品或其它交付物符合预期的要求。 78 | 79 |   规格说明文档在工程上是常用的必要文件,它经常被协调各方关于技术问题的整体的、正式的规范约定,是评价交付物质量的主要依据。 80 | 81 |   在工程以外,规格说明文档也可被作为公开的设计要求参考,而被不同的涉众实现。这种规格说明文档可能直接称为技术标准。 82 | 83 | ## 变量 84 | 85 |   关于“变量”这个概念的一般定义和含义,以及 C 和 C++ 语言为例的解释,参见[这里](variables.md)。 86 | 87 |   之前提到,要求变量表示可变状态(immutable state) 的[反驳意见](https://www.yinwang.org/blog-cn/2013/03/31/purely-functional)是站不住脚的。因为这两者历来就没有关系。偶然让“使用”变量和“使用”可变状态想通的,就是类似 C 语言的对象(object) 的概念。很遗憾,这个层面上首先就存在不少流行的理解偏差。 88 | 89 |   讽刺的是,不少半桶水 C 用户还信誓旦旦“C 没有对象”(而且很可能认为“C++ 有”),却不知道[一些他们讨论的语言中的基本常识](variables.md)——荒谬到值得在这里老调重弹一遍: 90 | 91 | * “对象”是 C 语言最重要且最常用的基本概念。 92 | * C 语言规范没有严格意义上的“变量”。 93 | * C 的对象和 C++ 的对象原则上是一致的。C++ 语言规范明确定义了“变量”,还包括不是对象的实体如引用(虽然和 C 一样不包括更一般概念上的“函数”)。 94 | * 硬要说 C 没有的“对象”,看来是来自“面向对象”的对象,特别是所谓原生支持面向对象特性的以“类的实例”作为“对象”的定义的 Java 这样的语言。但是就算清楚这些事实的,很大可能也不够清楚: 95 | * C++ 的“对象”的一部分——类类型(class type) 对象——完全符合这个“对象”的概念(虽然严格来讲,只是一类实现),甚至普遍实现中 C++ 的这种对象的一部分(以前的所谓 POD ,现在的 standard-layout class )在一定条件下的目标代码和 C 的结构体对应的目标代码二进制兼容。 96 | * Java 这样的 class-based OO 并没有无可争议地代表所有具有“对象”概念的面向对象语言(例如传统上基于原型(prototype-based) JavaScript )。发明“面向对象编程”概念的 Alan Kay 对这种认知偏差看来也有[很大意见](https://www.purl.org/stefan_ram/pub/doc_kay_oop_en)。 97 | 98 |   这个现状也一定程度体现出 C 的教学质量堪忧。而作为真正敢自称学会 C 语言(或者 C++ 语言)的用户,请做到不信谣不传谣,不要让谎言披上玩笑的遮羞布,口嗨了一千遍就变成了真理——这是对清楚事实的真正认真学习过基础的用户的侮辱。 99 | 100 |   “变量”替换“可变状态”多少也是类似的谣言。虽然不便追踪具体来源,根据对不同语言用户的观点的考察,有理由相信这是只使用过变量能表示可变状态的对象的语言的用户和/或 C 这样的语言学得不到家的用户以讹传讹的结果。这也能解释为什么反驳意见以 Robert Harper 笃信纯函数式“宗教”入手了,因为纯函数式语言就是最典型地存在不可变的变量的主要实例(如 Haskell 的 type variable )。 101 | 102 |   但是,这个动机根本是错的。避免变量和表示可变状态的对象的混淆,根本理由是混淆本身就会造成无理的问题。要说“变量”该是什么合理含义,那就该是保持最初的含义——关联可能可变的变量绑定的实体。这个概念在历史上的数学、程序设计语言理论和具体语言的认知都兼容——而且普遍包括“变量”和表示可变状态的对象等义的设计中。不顾这个事实,仅因为个别语言设计上的来源可疑的片面理解就去修改“变量”的概念,在这个意义上就是一种损人不利己的**寻衅滋事**: 103 | 104 | * **没事找事**地破坏普适概念对不同领域的兼容性,制造理解偏差和不同语言用户之间的交流障碍。 105 | * 那些因为设计者信谣而把“变量”意思领会错或者随大流等原因在语言规范中将错就错、以讹传讹的例子,应当作为反面教材,本不应该被理智的用户尊重,更不应该被纵容到必须让其它合理设计去兼容它的程度。反客为主宣扬向无知妥协才是“正统”——这是一种**反智**。 106 | 107 |   另外,严格意义上讲,实际问题更加微妙一些——以 [Robert Harper 使用的术语](https://existentialtype.wordpress.com/2012/02/01/words-matter/),所谓可赋值(assignable) ,是一种接受特定操作的实体,一般意义上并不意味着这种操作必须负责保证确保状态可变,因为状态是不是可变未必是局域特性,还可能是整个语言的语义统筹接管的。如 C++ 可观察行为不包括任意的状态修改;其中的 `std::is_assignable` 并不要求判断是否这样的操作实际修改了对象的值。当然,因为可修改(modifiable) 和可变(mutable) 其实有一些技术上的不同,这里仍然可以做到自洽。而这里的不同其实显示了当前语言设计普遍都不够抽象清楚计算作用(computational effect) 的问题;若是一直稀里糊涂地混用概念,洞察这些问题的可能性会显著缩小。不过,具体如何地有关,这就和“变量”的关系不大了。 108 | 109 | -------------------------------------------------------------------------------- /zh-CN/c-cpp-generic-type-system-defect.md: -------------------------------------------------------------------------------- 1 | # C & C++广义类型系统缺陷 2 | 3 | Created @ 2013-04-16, r2 rev 2013-04-16, markdown @ 2015-09-15. 4 | 5 | # 类型(type) 和 `const`/`volatile` 限定符(qualifier) 6 | 7 |   类型是程序设计语言中广泛使用的重要特性,被用于抽象数据和程序行为。类型之间遵循语言的若干语义规则,这些规则和类型一起被总称为语言的类型系统(type system) 。 8 | 9 |   C 语言具备较完整的静态类型系统,类型能够通过组合产生新的类型(数组类型、函数类型、指针类型等)。C 的类型对象类型(包括作为不完整对象类型的 `void` )和函数类型。C++ 在这个层次上没有太大改进,增加了引用类型,而把void类型排除在对象类型之外。 10 | 11 |   `const`/`volatile`限定符通过限定对象类型体现主要语义(但也用于 `void` ),是类型的组成部分。 12 | 13 |   关键字 `const` 在 20 世纪 80 年代引入 C 语言,表示禁止写入对象的只读语义,以便实现利用只读存储器[C++ D&E]。标准化 ANSI C 时使用了类似的语法和语义规则增设了 `volatile` 限定符,语义为读取被限定的对象存储值和写入值时也产生副作用,禁止实现缓存读取的值而减少读操作,能有效提供对I/O寄存器等的映射[C99 Rationale]。 14 | 15 |   而 C++ 中的 `const` 相对有只读以外更激进的语义:对于特定的类型构建(能在编译时确定的)常量表达式。这在某种意义上提升了优化的可能性,但造成了语义的复杂。(题外话,`volatile` 在 Java/C# 有其它不同的含义。) 16 | 17 | # 左值性(lvalueness) /值类别(value category) 18 | 19 |   ISO C 以及 ISO C++98 和 ISO C++03 的左值性(lvalueness) 是一种二值系统:C的取值分为左值(lvalue)和非左值;C++中分为左值和右值(rvalue) 。任何表达式的左值性取值都是二者之一[ISO/IEC 14882:1998, ISO/IEC 14882:2003]。 20 | 21 |   ISO C++11 的值类别(value category) 是左值性更复杂的演进,基本类别构成三值系统:纯右值( prvalue ,相当于 C++03 的右值)、消亡值( xvalue ,新增的值类别)和左值(lvalue) 。其中纯右值和消亡值合称右值,消亡值和左值合称泛左值(glvalue) 。任何表达式的值类别取值都是三种基本类别之一[ISO/IEC 14882:2011]。该演进主要是因为右值引用的引入而显得必要,详细动机参见[ISO/IEC JTC1/SC22/WG21 N3055]。 22 | 23 |   左值性来源于 B 语言需要对表达式的左操作数的做出语义限定的需要:只有左值(left-value) 才能作为“=”、“++”和“--”的左边(……以及“&”的右边)——此时 lvalue 显式地作为文法元素;类似地,出现在“=”右边的元素相对称为 rvalue[User's reference to B]。左值的概念后来被沿用至 C 和 C++ 语言。 24 | 25 |   由于左值也同时被作为 `&` 操作符的操作数的语义限定条件,实际上表示可能在内存中保持的对象——存在对用户可知的对应的内存位置(memory location) ,lvalue 也被叫做 locator value ;而C中所谓的右值(rvalue) 一般即为值(value) 的同义词[ISO/IEC 9899]( C++ 的值则可以是实现定义的其它概念)。 26 | 27 |   注意 C 和 C++ 的左值性在函数类型的表达式上有差异:C 的函数指示符(function designator) 不是左值也不是右值。而 C++ 的函数名是左值。 28 | 29 | # 左值变换(lvalue transformation) 30 | 31 |   以上讨论的是原始的左值性/值类别。在 C/C++ 中,通过特定的、仅有少数求值(语法)上下文例外的隐式转换,转换的结果的表达式的左值性/值类别可以改变,同时类型也可以改变。 32 | 33 |   这些转换在 C++ 中为便于重载解析(overloading resolution) 描述而被统称为左值变换,包括三种标准转换(standard conversion) :左值-右值转换(lvalue-to-rvalue conversion) 、数组-指针转换(array-to-pointer conversion) 和函数-指针转换(function-to-pointer conversion) ,其中值类别由泛左值转换为纯右值(对应 C++98/03 中左值转换为右值),同时后两者有类型变换,分别为数组左值 e→&e[0] 和函数左值 e→&e 。 34 | 35 |   转换被排除的少量上下文是作为(按 C++11 的说法)非求值操作数(unevaluated operand) 时、操作数需要左值时以及初始化左值引用时。 36 | 37 |   在C中也存在类似转换,排除的上下文也类似(当然没有用于初始化引用的情形),但只有第一种被明确命名为左值转换(lvalue conversion) (\[ISO/IEC 9899]) 。 38 | 39 |   左值性/值类别虽然不是 C/C++ 的名义上的类型,但从目的(静态分析区分操作、转换以及检查类型错误)来说,符合静态类型系统的一般特征。因此本文称为广义类型一致处理。 40 | 41 | # 左值、限定符和引用类型:冗余和复杂 42 | 43 |   一个主要的问题是左值性和 `const`/`volatile` 限定符(作为类型的组成部分)的不完全正交组合造成的冗余。 44 | 45 |   对于 C 以及 C++ 中排除类类型(class type) 外的非左值表达式,`const`/`volatile` 限定符被自动丢弃。这样,存在5种等价类: 46 | 47 | * 非左值:不可写、读不产生副作用; 48 | * 左值:可写、读不产生副作用; 49 | * `const` 左值:不可写、读不产生副作用; 50 | * `volatile` 左值:可写、读产生副作用; 51 | * `const volatile` 左值:不可写、读产生副作用。 52 | 53 |   显然,这里非左值和 `const` 左值在写权限和副作用是重复的,除了作为 `&` 和若干转换的操作数。 54 | 55 |   对于 C 来说,这样的区分尚且是有意义的:`&` 能返回 `const` 左值的指针来间接获得所需的值,代价是需要占用存储;而非左值则只能立即使用值,但不需要被储存。 56 | 57 |   而在 C++ 中,这样的设计就有些匪夷所思了:明明存在能绑定右值的 `const` 左值引用来获取右值对应的值,为什么还需要 `const` 左值? 58 | 59 |   对于 C++ ,另一个冗余是,引入(左值)引用表示的左值,除了存储以及参与不同的重载(但显然能和对应对象类型产生歧义)外和对象左值没有区别,这对于内建表达式基本没有意义,保留不相同的规则造成不一致性。 60 | 61 |   事实上,C++ 中,(左值)引用正在取代传统非引用类型的左值的地位,早在 1992 年标准化开始时的第一篇公开论文[X3J16/92-0053 WG21/N0130]就提议这点,并在之后各种场合改头换面、缺乏直觉规律地出现(如模版左值类型推导、`decltype` )。但内建操作符表达式左值的类型并没有修改(比如内建赋值和前置返回引用),不和其它 C++ 特性一致而和 C 保持一致。这点很容易被忽视而导致一些误解。 62 | 63 | # 可修改(modifiable) 左值的意义 64 | 65 |   在 ISO C/C++ 中,直觉明确的可写权限并没有被定义,而是规定了对左值的可修改(modifiable) 的概念,事实上表示可写左值的子集。 66 | 67 |   这个概念的一个重要应用是禁止数组左值作为赋值的左操作数:ISO C中数组类型的左值就明确不是可修改的。但事实上并没有明确违反语义规则时究竟有没有已经左值转换(转换为右值不能在“=”左边——如果这点成立,可修改左值的规则在这里是冗余了)。这种含糊的归类导致标准在起草措辞(wording) 上的一些问题,例如在 ISO C++ 中遗漏了明确的数组左值的可修改性表述(虽然可以推测合理的情形是和 ISO C 一致),可能是潜在的缺陷。 68 | 69 |   另外,对于位域(bit-field) 类型,可能是因为按字节地址寻址的困难性,尽管仍然能作为可修改的左值,也需要额外明确。可修改在语义上无法帮助减少这里的措辞。 70 | 71 | # 为什么 C/C++ 在这里不够理想:解决的困难 72 | 73 |   抛开现有的语言特性,从设计新语言的角度看,相关的基本需求不难总结: 74 | 75 |   作为基本抽象,对象或者值需要可读; 76 | 77 |   不同于所谓“纯”函数式语言,能够保存状态,允许(但不滥用)副作用带来的表达计算的便利是 Algol-like 语言的基本需求之一; 78 | 79 |   作为能够容易操作硬件的语言,需要类似使用 `volatile` 的机制提供映射硬件存储的能力。 80 | 81 |   下面可以证明满足以上需求的设计可以比现有的更简单,而不必要有如此多的冗余和模糊(注意,适合实现或者通过现有语言演进,是另外一回事)。 82 | 83 |   显然,不完全正交的广义类型不如合并为有以下分类的一个类型系统显得更清晰: 84 | 值:可写、读不产生副作用; 85 | 86 | * `const` 值:不可写、读不产生副作用; 87 | * `volatile` 值:可写、读产生副作用; 88 | * `const volatile` 值:不可写、读产生副作用。 89 | 90 |   这种方案要求需要对所有值一视同仁地提供 `&` 等现在的左值具有的操作,若不考虑彻底放弃或加上使问题更复杂的其它限制的话。(生存期需要另外一些规则,但相比之下不是太大的问题。)表面上看,这阻碍了值“不要求存储”的优化;但是除了 `volatile` 类型外,关于抽象机的可观察行为的等价语义可以很容易挽回这一点,至少对于 C 来说最终只有一点关键的阻碍—— `&` 的结果是什么? 91 | 92 |   基于现在的 ISO C/C++ ,至少理论上并非不可解决:内建&的结果是一个指针类型的非左值,一个重要的语义限制是表示“地址”——但地址相关的可能的可观察语义(指针算术以及值的标准输出)是实现定义的。尽管改变现有的、典型的把地址的取值范围映射至某个地址空间的实现附加的实现复杂性会是非常致命的。 93 | 94 |   但是更致命的是,关于“地址”和“内存位置”等存储实现的(反)抽象:按特定基本单位(字节)连续的存储。这种过于具体的约定换来符合现有体系结构的可实现性(以及关于“布局”的控制,如 `offsetof` 的易实现性),却为类型系统的简化制造了麻烦,同时削弱了用户对象类型的控制:无法抽象出不确定布局但占用存储的类型,也没有能力让用户直接使用标准的手法简洁一致地安排布局。(非常讽刺的是,C++11 在内存模型的定义上更严谨更全面,严格意义上的限制却也更大了。) 95 | 96 |   C++ 的问题更严重些。一个问题是引用类型初始化的非对象复制初始化语义。这点其实也可以通过规定激进的非 as-if (可观察行为)语义来解决(现在ISO C++关于特定的复制/转移构造就这样做,甚至可以无视副作用),虽然这样总体上的语义或许会更模糊,让用户更无所适从。另一点是,在内存模型和布局上一直无法完全由用户控制(如对于有虚函数的类,通过 `offsetof` 计算数据成员地址就无法保证结果可预期),这同时具有贴近现有体系结构实现和便于高层抽象的优点,但另一方面也可以说都是缺点。 97 |   最后还有一个非常现实的困难:兼容性。假设 C 或者 C++ 确实能通过整体改换设计(幅度显然比当年 C 到 C++ 大得多)解决了这样的问题,新的语言规范如何适应旧的程序?如何使用户能够顺利迁移?从现状( C/C++ 的用户,以及现有需要维护的项目)来看,这是明显不实际的。 98 | 99 | # 结论 100 | 101 |   基于以上讨论可知,要消除类型系统的冗余带来的负面影响涉及过多的核心设计,从现有语言为起点立即改进这些缺陷可以认为是不可行的。对于用户而言,尽量清晰掌握核心概念完成任务是比较实际的。但对于语言自身的维护而言,这个问题导致加入新的语言特性、使不同特性有效协作的难度大大增加,需要长期努力才有望缓解。 102 | 103 | -------------------------------------------------------------------------------- /zh-CN/combinator-critique.md: -------------------------------------------------------------------------------- 1 | # 组合子语用批判 2 | 3 | # 引言 4 | 5 |   近年来在讨论编程语言及其教学的议题中有少数不同观点。 6 | 7 |   这其中有一股值得注意的历史逆流。 8 | 9 | # 样品 10 | 11 | [知乎回答](https://www.zhihu.com/question/30467199/answer/490183958) by bhuztez 。 12 | 13 |   表达个人观点,整理和保存资料并非具有冒犯性,在普遍意义上是对公众有益的。但是,为了个体好恶而无视一般人具有的更深层次的利用计算的方式,鼓吹个别风格,则是无益的。 14 | 15 | # 问题分析 16 | 17 | > 未来是属于以APL为首的组合子邪教的,括号家族的好日子,我看也快看到头了。 18 | 19 |   括号是否需要到头暂且不论,但基于对正常认知的尊重,必须旗帜鲜明地指出,滥用组合子的风格,本身就是一种邪教。 20 | 21 | ## 概要 22 | 23 |   组合子在此的最重要的性质是**排除一般意义上的用户定义的符号指称**,具体点说,它禁止任何允许用户引入和上下文相关的变量。 24 | 25 |   与组合子风格对立的,并不是所谓的 LISP ,而是所有**支持带作用域规则的变量**的语言设计。 26 | 27 |   这种设计最早被 A. Church 在 λ 演算中较完整地形式化,所以对立的源头倒是没错。但正因如此,反映了该文作者在相关领域欠缺历史常识:λ 演算的流行并不是因为语言设计的倾向性,而是**任何实用到需要允许用户引入变量的语言(不论其设计者是否意识到),都会在操作语义上落入 λ 演算及其变体**。 28 | 29 | ## 可用性 30 | 31 |   带有作用域规则的变量在 λ 演算中以 λ 抽象(lambda abstraction) 的形式提供,对应于一般语言设计中的用户自定义的过程和函数。λ 抽象就是这种实体的构造器。与此相对,组合子本身并不允许这种构造器,而只允许排除依赖上下文的符号的输入。这给人类用户使用编程语言带来普遍的困扰。 32 | 33 |   应该注意,在**表达计算**上,λ 抽象不是必要的,如纯粹的[组合子逻辑](https://zh.wikipedia.org/zh-cn/%E7%BB%84%E5%90%88%E5%AD%90%E9%80%BB%E8%BE%91)可以具有同等的可计算意义上的表达能力。 34 | 35 |   但是,在**抽象能力**上,两者并不等同。缺乏抽象支持的纯粹组合子系统显著缺乏一些抽象能力。技术上,不需要考虑“抽象”的精确定义,这已体现在: 36 | 37 | * 组合子逻辑的语言相对并不容易表达一般的逻辑,因为这依赖在已知组合子中判断选取是否能符合程序目的的子集。 38 | * 人类用户在这个意义上普遍是弱势的。 39 | * 依赖寻找恰当的组合子进行编程,和在语法分析树中暴力搜索一个程序的难度近似。 40 | * 对人有意义的、不限制长度的一般组合子程序往往通过对应的 λ 演算的程序基础上消除(eliminate) 抽象得到。一个例子是 [unlambda](http://www.madore.org/~david/programs/unlambda/#lambda_elim) 。 41 | * 相反,使用类似 λ 抽象的自由变量这样带有上下文的变量,重写系统规则允许人类编码不影响程序语义但可能具有附加意义的等效编码。 42 | * 具体地,λ 演算允许对自由变量 α-重命名等价。这对应一般编程语言中,变量命名不需要影响程序的语义(尽管可以附加人为的其它限制来约定不同的命名风格允许出现的上下文)。 43 | * 这种性质允许代码的作者选取具有对人类读者特定含义的普遍命名,而不需要把这种含义在程序中以符合语言的语义规则的构造完全地实现。 44 | * 这种分离实现构造和接口意图的做法,允许用户引入抽象表达意图。 45 | * 不需要使语言的语义依赖程序的完整构造使语言能够具有非平凡的等式理论(non-trivial equational theory) ,使程序的局部片段能够被单独翻译而不影响这些片段组合之后的语义,这种性质允许了一类最一般意义上的程序优化。 46 | * 纯粹的组合子的表达并不蕴含允许具有不同符号但附加不同意义的程序。除去上述计算上的反人类设计的问题无法简易解决,缺少的抽象以组合子的语用方式也基本无法挽回,而且消极影响会更加严重。 47 | * 如果要克服缺少抽象带来的作用,需要约定程序构造来作为缺乏命名接口的变通。 48 | * 若只使用组合子自身的语法——应用组合子的并置连接(juxtaposition) ,这会直接限制组合子之间出现的顺序。这样,同样意图的组合子为应对“重载”需求需要附加编码和领域逻辑无关的片段,进一步混淆了接口和必要的实现。 49 | 50 |   这些理由导致缺少抽象的语言不适合人类用户普遍使用。 51 | 52 |   注意缺少抽象的缺陷根本是语义规则的问题。至于括号,只有线性语法消歧义时才需要。如果抽象语法树直接表示为可视化编辑器中的区域,或者使用允许代码的视觉呈现带有区分片段的颜色等方案,那么不需要括号就能消除歧义。 53 | 54 |   比较语言的抽象能力被作为正式的课题研究,如[抽象理论](https://web.cs.wpi.edu/~jshutt/abstraction-theory.html)。 55 | 56 | # 认知偏差 57 | 58 |   该文的作者在人对计算认知的历史和相关专业知识上也存在几乎算是全外行的偏差。 59 | 60 | ## 历史问题 61 | 62 |   作者认为,Turing 和命令式编程没有直接关系,有关系的是 von Neumann 。这是普遍的误解。 63 | 64 |   实际上,这两人跟编程本身的范式都不大。反倒是 Church 的工作的确和函数式编程有些关系。 65 | 66 |   Church 和 Turing 两人为了解决关于可计算性上的问题分别提出关于计算的模型。一个重要的区别是,Church 提供的 λ 演算作为一个演绎系统(deduction system) ,自身可以作为语言来用,并且和普通的代数方式衔接得较好。事实上日后 John McCarthy 设计 LISP 时,直接拿来了 Church 的部分设计,[是因为计算表达的简易,并没有考虑很深刻](http://jmc.stanford.edu/articles/lisp/lisp.pdf)。所以这个意义上 Church 的确影响了编程语言的设计和使用范式,尽管这不是他的本意。而 Turing 提供的模型并没有被作为语言使用。 67 | 68 |   至于 von Neumann ,主要贡献是和当时一些其他人一起整理和提出了程序和数据统一执行的 Princeton 架构,这是计算机的模型,比计算模型离抽象的语言还更远。 69 | 70 |   命令式编程和程序存储被计算机解释的指令的计算机实现相关,但这并非 von Neumann 的发明(Zuse 的 [Z1](https://en.wikipedia.org/wiki/Z1_%28computer%29) 就使用可编程指令)。而早年流行的串行解释指令在之后的发展中也被证明不必要,现代的命令式编程也不那么强调按字面顺序默认求值。 71 | 72 |   一个关键问题在于,按 von Neumman 的设计,代码和数据并不需要具有体系结构上的原则差异,那么表示代码的指令自然是数据的特例,而不是相反(除非不允许程序存储数据,此时所有的数据都必须编码在指令中)。硬说的话,LISP 的代码即数据反倒是体现了 Princeton 架构,而 C/C++ 之类更多 ALGOL-like 语言则原生地更接近程序代码和数据分离的 Havard 架构。 73 | 74 | ### 范式 75 | 76 |   同理,程序是表达声明还是命令,普遍是用户的决定。一个充分成熟的设计上并不需要被特别关心,特别是一个通用语言,几乎总是该容忍用户对不同范式的混用。 77 | 78 |   **而通常被认为命令式风格的典型设计,包括语句和一些控制操作,都是更普遍的过程式语言的特例。** 79 | 80 |   注意,虽然历史上计算机实现的“过程”晚于指令出现,但逻辑上整个程序生存期可以被视为活动的过程,只不过不需要考虑程序内部的调用者。所谓的指令或者更抽象语句,说白了只是附加了控制操作符这种抽象过程的程序片段的组合罢了。 81 | 82 |   具体来说,过程式语言的一般构造是可具有递归构造的表达式,计算以表达式的求值表示。表达式的求值可能蕴含子表达式的求值。求值可具有计算作用,包括确定计算结果和副作用。 83 | 84 |   像 ALGOL-like 语言所谓的语句,是保证表达式求值顺序符合语法定义的单一顺序的控制操作的语法糖;把语句视为蕴含子表达式的表达式,这种操作是作用在子表达式上一种控制操作符。事实上在 Scheme 中,这种操作符以特殊形式 `begin` 体现,本身并不需要特别的语法设计。可见“语句”的概念是特殊(多余)的。 85 | 86 |   其实 Scheme 在减小设计冗余上也不咋地,RnRS 用的实现是 `lambda` 里写死的(然而原始的 lambda 并没有这个意思,毕竟纯函数式嘛都不在乎子表达式求值顺序),而更具有扩展性的设计(如 [Kernel](https://web.cs.wpi.edu/~jshutt/kernel.html) )允许把类似的操作分解为其它实体。 87 | 88 |   另外,强调操作语义,任何语言首先都是“命令式”的;传统上“声明式”的设计,总是需要一个类似事件循环的外壳套着驱动解释来表现程序行为,就像处理器加载程序解释指令一样。单说声明式的部分,那还不如直接当作一种源代码输入格式来讲比较贴切。 89 | 90 | ### 方法 91 | 92 |   为什么要采取这种视角?因为这是所有编程语言的**操作语义**事实上的统一构造。**其它的形式都会有更多的冗余。** 93 | 94 |   这种方法本质上复用了描述系统规则的元语言和被描述的系统中对象语言的规则。其它的形式语义方法,没法直接统一计算机模型的实现,同时作为计算模型和计算机模型,而需要多出定义计算到计算机的映射。 95 | 96 |   λ 演算作为计算模型和编程语言的模型就是这种方法的例子之一,尽管这不是设计者的本义,因为不支持副作用实用性也受限。LISP 则是更实用的例子。相对地,组合子演算可以作为计算模型,但距离计算机自然地更远,仅凭这个理由就不可能有 LISP 的历史地位。顺便,[Forth](https://en.wikipedia.org/wiki/Forth) 这样的语言可能算是组合子逻辑在接近计算机而远离通用计算模型上的对偶。至于更常见的 ALGOL-like 语言,那只能说大杂烩,不说什么光写个随便什么方法的形式语义都够呛。 97 | 98 | ## Ultimate 99 | 100 | > 这就涉及到第一行提出的一个问题,λ是不是the ultimate? 101 | 102 |   历史常识问题。[Lambda the Ultimate](https://softwareengineering.stackexchange.com/questions/107687/what-is-the-origin-and-meaning-of-the-phrase-lambda-the-ultimate) 原本鼓吹的还只是表达问题而不是抽象能力问题。“没必要使用其它计算模型”(怼的是[当年的 actor model](https://en.wikipedia.org/wiki/History_of_the_Actor_model#Scheme) )还是隐藏在另外的设计(也就是 first-class continuation )中的。(讽刺的是,OOP 党照搬 actor model 话术的时候把里面的“消息”之类的意思弄拧了。) 103 | 104 |   要是考虑到抽象,那更加没组合子之类什么事了,毕竟组合子就是“排除抽象”出来的。 105 | 106 |   有意见,还可以考虑把 [LtU](http://lambda-the-ultimate.org/) 掀了。 107 | 108 | ## FP 109 | 110 | > 在这些非LISP系的FP里面,比较典型的有两个,一个是prolog,另一个是APL。关于这两个,感兴趣的大家可以自行科普,也可以评论留言。 111 | 112 |   原本的 LISP 设计只是为了强调简单地表达数学意义上函数的计算;之后的实现更接近一般意义上的过程,因此算不上很 FP ——上面的范式也是建立在这一点上。 113 | 114 |   之后自称 functional 的往往是所谓纯 FP 也就是排除支持副作用,这里最明显的派阀是 ML 。纯 FP 里 ML 是 eager 的,Haskell 是对面 lazy 的代表。这哪轮得到 Prolog 和 APL 了? 115 | 116 |   硬说的话 Prolog 反而可以单独划分出 LP(logical programming) 但明显更加 DSL 。APL 是资格老但反而是这里提到过的语言里最没存在感的,不知道是不是用户体验的问题(至少需要特殊键盘输入这点大部分用户似乎都觉得糟心)。 117 | 118 |   至于现代 Lisp 方言是普遍不排除副作用。Scheme 相比更不鼓励副作用,Common Lisp 之类更接近命令式语言的习惯。 119 | 120 |   说到底 FP 很大程度是生造的概念。虽然 John Backus 当年是有搞过叫 [FP 语言](https://zh.wikipedia.org/zh-cn/FP_%28%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80%29)的,但除了强行一个参数和现在流行的少部分所谓 FP 语言类似外,其它设计差得很远,甚至背道而驰(比如强调所有函数都具有一种类型)。至于 LISP 要比这个早得多,不知如何讹传上的。Lisp 方言方面,Scheme 自称过程式,Common Lisp 等自称多范式,本来也算不上多 FP 。因为随便一个过程式语言都能支持数学意义上的函数,FP 就变得愈发不明了。或许 FP 的剩余价值在强调函数作为一等对象(first-class object) 这种正常语言本该具有的特性;不支持一等函数的语言就是非 FP 的。 121 | 122 | ## FP 和 OOP 123 | 124 |   FP 和 OOP 的同源就是都属于结构化的过程式的范式。其它的,只能说 OOP 套用的概念更加不明所以,比 FP 的内涵和外延不清晰更加具有杀伤力。 125 | 126 |   另外,[OOP 有专用的操作语义模型](https://stackoverflow.com/questions/3113368),但实际上都不怎么样,现在学术界都过气了。像所谓 object calculus 的 sigma 抄的 lambda ,并没有所宣称的“更简单”,又不符合大多数实际使用的 OOP 语言,还不如用 lambda 和 record 来实现 OOP 。 127 | 128 |   究其根本,流行的 OOP 特性都是些事实上更高层的东西,而要直接表达计算的核心本来就用的是过程式那套,也就是 λ 演算的变体。只有不明所以的一开始就把 OOP 想成新鲜基础的外行才会徒劳地妄图逃脱 λ 抽象的统治范围。就算山寨成功,撑死了也就是一个 λ 演算计算体验接近的方言;作为逻辑模型缺乏对应的传统逻辑的基础,也没逻辑学家会鸟;更别指望扩展出 [λ 立方体](https://zh.wikipedia.org/zh-cn/Lambda%E7%AB%8B%E6%96%B9%E4%BD%93)之类。 129 | 130 |   的确存在和传统风格不太一样的其它扩展 λ 演算的[不同方法](https://www.wpi.edu/Pubs/ETD/Available/etd-090110-124904/),但跟区区 OOP 没什么关系。 131 | 132 | ## LISP 和 OOP 133 | 134 |   LISP 和 OOP 没什么直接关系,但 Common Lisp 的 [CLOS](https://en.wikipedia.org/wiki/Common_Lisp_Object_System) 是早期到现在的一个重要 OOP 语言支持实现的代表。CLOS 比大多数 OOP 语言内建的功能更强,比如支持方法多分派。 135 | 136 |   记得《C++ 语言的设计和演化》里 Bjarne Stroustrup 还提过参考了一些 CLOS 的设计。 137 | 138 | # 实用性 139 | 140 |   到这里,可以考虑一下组合子真要作为严肃的编程语言构造,发展如何。 141 | 142 |   组合子被作为“不带有自由变量”的 λ 抽象的副产品被发现具有普遍计算表达能力的构造是在 λ 演算发明之后,但显然早于 APL 。如 SKI 这样的组合子演算系统,只提供了少量“标准”组合子。 143 | 144 |   那么照理说,作为实用语言的 APL ,应该派生更多的组合子(以现有的组合子实现这些组合子)来体现设计在表达计算上的有效性。但是,实际上,APL 提供了大量的和程序派生无关的组合子,并没有把它们作为能构造其它计算的基础。这种用法只是常规函数上的限制,在证明可用性(尤其是可扩展性)上远远不具有早期 LISP 的公理化操作符具有说服力。 145 | 146 |   由于组合子之间的派生关系并没有被 APL 这样的语言明确,如何选取组合子就成了用户自己的负担。 147 | 148 |   需要注意的是,自然语言的学习经历普遍不能克服这个困难(且不提自然语言在避免歧义为前提的抽象上相对大部分允许自由变量的人工语言来讲体验极差)。因为组合子之间的可派生性往往非常抽象(且经常缺乏变量名作为暗示),这不像是自然语言中的许多词汇可以由并行累积学习后融会贯通然后潜意识地消解构词上的重复,因此用户需要直接记忆映射符号和具体含义的“词汇表”。不凭借外部实现的知识,这种词汇表在任何时候都难以被压缩。除了具体含义(目的)的存在性,这种词汇表的内部组织形式脱离 APL 这样的具体语言规则后难以复用,空间效用远远不如自然语言,堪称**垃圾知识**。这种用户精力和注意力的浪费和剥削又进一步限制了用户关心如何使用组合子有效派生出程序的可编程意图上。 149 | 150 |   上述种种问题技术上几乎不可能由当前可预知形态的组合子语言解决。因此,组合子以及所谓的[无点(point-free)](https://zh.wikipedia.org/zh-cn/%E9%9A%90%E5%BC%8F%E7%BC%96%E7%A8%8B) 风格在主流编程语言(不管是否通用目的)普遍只会作为“不带有自由变量”的特例,而不是唯一选项。(题外话,“无点”并不容易理解,但叫做“隐式编程”容易带来更大的混淆——隐去的只是指作为函数的变量名,远远不能涵盖其它风格。) 151 | 152 |   适当强调组合子替代无谓的抽象构造可能有助于程序的简化,但并不能作为基本的操作。例如,C++ 的 `std::bind` 不能代替 lambda 表达式,而只能相反(虽然后者也不被无条件地鼓励,因为使用组合子的方式在特定上下文仍是有益的,例如有利于建立表达不关心自由变量这种实现细节的更高层次的抽象)。 153 | 154 |   组合子风格作为计算模型的基础有历史上的合理性,但依赖这种基本特性构建编程语言,鼓吹其普适性却无视无法解决的固有缺陷,不论是否自欺欺人,都是对人类用户的普遍挑衅。 155 | 156 | ## 技术理由 157 | 158 |   当然,这种企图并非个例。大多数动机是出于“更容易使用”,如 John Backus (1977) 鼓吹的[函数级编程](https://zh.wikipedia.org/zh-cn/%E5%87%BD%E6%95%B0%E7%BA%A7%E7%BC%96%E7%A8%8B) : 159 | 160 | > 编程语言似乎遇到了麻烦。每种后续语言,虽做了一点清理,都合并了它的前任们的所有特征,并加上了更多的特征。[...] 每种新语言都宣称了新的和时尚的特征... 但是明显的事实却是,很少有语言使编程者能足够廉价或更加可靠的去验证生产和学习使用它们的代价是值得的。 161 | 162 |   这种观点的问题在于: 163 | 164 | * 强调纯粹的函数级编程,即无点风格,相比其它普遍流行的高级语言,要求在语言中取消(或添加上下文限制)引入的变量名指称实体的能力。这和在语言中取消(或仅限个别上下文)引入副作用的能力是类似的,导致**需要解决本不需要存在和解决的问题**。 165 | * 这是放弃既有高级语言普遍有效的设计,而改用抽象上更低级的语言来实现表达计算的意图的实例。这种路线的价值在方法论上极其可疑。 166 | * 相比既有的语言,这样的“纯粹”设计显然地不实用。因为变量名和副作用这样的设施在解决通用领域问题中的普遍性,排除这些设计通常就是把如何引入这种特性的问题丢给了语言的用户。然而经验表明,在缺乏直接支持这些特性的语言中,用其它特性组合出可用的设施实际上并不容易——至少并不容易做到易于无冲突地表达意图。 167 | * 如先前指出的,程序语言理论的发展将允许精确地刻画描述抽象能力。即便如此,不太可能推翻以上经验性的结论:既有的语言在这些最基本的普遍设施的设计上,相当接近理论可能允许的平衡设计简单性和普遍可用性的需求之间的最优解。因此,不论使用何种具体设计,只要实现从既有的设计中去除这些设计的企图,极可能总是会自然地损害整体上的实用性。 168 | * 而增加一个不能完全取代适用旧有场景新风格不会使语言容易被验证和学习,反而容易增加学习和选型成本,特别是两种风格不总是容易互相迁移的情况下。 169 | * 同时支持不同风格的语言中,容易存在互操作性问题。这还容易引入本不相关的翻译阶段(phase) 依赖而引起新的心智包袱。 170 | * 注意语言实现不是强人工智能,不可能彻底理解使用语言实现的完整意图,原则上无法代替人类用户实现决定如何命名的策略,因此不可能完全地(在语义上无损地)自动变换迁移不同的接口设计和实现风格。 171 | * 一个另外的典型的例子是 [Boost.Hana](https://www.boost.org/doc/libs/release/libs/hana/) 试图解决 C++ 中“类型级”不如原有的更一般的“值级”计算在表达上更自然的问题。而它仍无法解决的“类型级”弱于“值级”计算在抽象能力(特别是表达副作用能力)上的缺陷。 172 | * 实际上一般意义上的无点风格在调用中传递和返回函数隐含一等函数的支持,所以[被当作传统函数式风格的一部分](https://dl.acm.org/doi/10.1145/72551.72554)也并不奇怪。反而是刻意区分不同的风格来划分语言子集的做法缺乏显著必要性。 173 | * 计算模型上也不必要,详见以下相关讨论。 174 | * 函数式以外的场合中,滥用具体风格可能放弃语言更普遍的良好定义的性质。 175 | * 惯用无点风格除需要表示操作的原语(primitive) 外,可能更依赖运行环境的结构而泄漏抽象,而损害整体的抽象性。 176 | * 这样的设计通常难以免除用户想象作为细节的具体资源分配(而不能直接把资源以对象代替),例如不得不关心一个栈在计算的什么阶段取得了什么参数。相对地,类似 λ 演算的语言直接使用纯粹的重写系统,在计算结果外只要考虑副作用即可。 177 | * 一个例子是 Forth ——和 LISP 类似地同样具有特色鲜明的求值规则,但和 LISP 不同,抽象更为薄弱。 178 | * 片面地鼓励的上下文相关的构造,为了配合参数的使用顺序,可能强加任意的文法复杂性。 179 | * 例如,为了[避免调整特定操作数的顺序而不惜引入消歧义](https://www.zhihu.com/question/55490625/answer/145033672),使分析依赖语义而取消语法的独立性。 180 | * 忽略了鼓励构造无点风格的函数的实质——滥用消除参数后的调用形式的等价性(在 λ 演算中的 η-等价)——而具有限制和代价。 181 | * 这直接限制参数的可能附加的计算作用,而影响抽象的可用性。 182 | * 例如,RAII 资源不能直接被消除,因此和无点风格不兼容。 183 | * 保持等价的变换是非局部的。不论需要向无点或常规风格调整,调用者和被调函数都可能需要同步修改。 184 | * 滥用无点形式的代码可能难以被理解。 185 | * 当存在逻辑上需要同时处理的多个参数时,隐去参数会造成理解的困难而破坏可读性。 186 | * 特别地,在没有类型签名辅助推导的情况下,理解代码的合理手段就是转换为带有参数变量名的通常形式。 187 | * 存在不少可以被理解但可用性存疑的情况,难以明确必要性。 188 | * 参数变量名明确是语法噪音时,无点风格才明确更好。这种情况在多数领域中较少出现。 189 | * 无点风格可减少不必要的参数命名。但是不使用无点风格,对参数进行命名也不应有困难。 190 | * 无点风格可能使程序更简短。但对同步修改多个函数的情况,这种判断可能难以简便和正确地实现。 191 | * 更多时候,这种风格直接造成实现困难,因此需要限制使用。 192 | * 无变量(variable-free) 风格根本不能表示需要被复用的同一计算结果,除非这些实体的引入和消除没有计算作用而总是视为同一;但后者的冗余变量本身就因为分散关注点而有碍可读性,几乎总是不被鼓励出现。 193 | * 函数的构造可以明显更困难。 194 | * 一般的情况下,逻辑上函数可能需要多个参数作为输入,不易直接构造无点风格的写法。 195 | * 对鼓励这种风格的设计,语言需要限制函数参数,而另外调度其它参数。这种限制这在 John Backus 最初的 FP 语言和一些现代函数式风格的语言中都有体现。 196 | * 若使用 Currying 调度参数,调用的形式很容易不必要地复杂。 197 | * 受到控制操作符影响,有条件使用的参数不容易构造为无点形式。用户可能需要人工进行控制流分析。 198 | * 大多数语言中不限制无点风格的程序,但用户并不普遍使用这种风格,也说明应用场景有限。 199 | * 考虑计算模型的性质,只使用这样的设计的语言在抽象能力上和实现的有效性上都是普遍较为薄弱的。 200 | * 无点函数风格对应的计算模型上普遍具有劣势。 201 | * 无点函数风格不使用 λ 抽象,自然也不使用 λ 抽象的应用以(可能依赖环境的)替换作为形式参数的变量来定义函数。这更接近 [μ-递归函数](https://zh.wikipedia.org/zh-cn/%E9%80%92%E5%BD%92%E5%87%BD%E6%95%B0)。 202 | * 相对 λ 抽象定义的 λ-递归函数,它具有取消变量和以及与传统的逻辑系统关联性的劣势,也缺乏可实现性和抽象能力上的明确优势,因此除了基于自动机理论讨论复杂度类外实际上很少用到。这样的模型来指导实用语言设计的必要性上非常可疑。 203 | * 此外,类似的模型自身严重缺乏如 λ 演算的扩展性:通过修改局部规则构造出新的实用模型的能力。 204 | * 除去类型系统添加进一步限制的有类型 λ 演算,在 λ 抽象上直接补充细化实现替换的结构和需要满足的附加约束,以及补充状态和控制作用都可以得到保持一定重要可用性(如 Church-Rosser 性质)的模型(如[先前提到的扩展](https://www.wpi.edu/Pubs/ETD/Available/etd-090110-124904/)的例子)并描述实用编程语言。 205 | * 在缺乏抽象构造器作为中间层的其它演绎系统中,没有如此多的成功应用于编程语言中的例子,甚至能保持多少有用性质都显得可疑(就先无视相对于 λ 演算少了抽象的问题好了)。 206 | * 区分函数级程序和值级程序在元语言中并非必要。 207 | * 即便无变量风格的构造器使用完全不同的规则,同为函数构造器,它仍然能被嵌入到以 λ 演算作为基础的系统中,这样的系统描述程序级和值级共存的语言,但这个意义上它就是一般意义上的支持一等函数的现代函数式语言——即便写到元语言里也不是静态的,用对象语言的通过使用 λ 抽象实现的机制替代也可以无法分辨。 208 | * 而更加纯粹的、限制更大的、拒绝嵌入具有 λ 抽象的纯函数级语言,才需要原始的(更弱的)不包含 λ 抽象的系统描述。 209 | * 就语言设计来讲,即便其函数定义排除了变量替换,为表达求值规则,一样需要在元语言中引入替换的规则,起草语言规范时整体并没有显著的简化,反而无法在对象语言中可用。(元语言的不必要的单独复杂性这也是 Lisp 方言外绝大多数语言的通病。) 210 | * 对实现来讲,缺乏能在函数内局部使用的环境和名称解析的支持使之自举更为困难,实现 meta-cirtualar evaluator 这样的自解释机制更加困难,使动态程序翻译需要有更多的冗余,而更难彻底复用现有的运行时实现。 211 | 212 | # 语法 213 | 214 |   另外,[Notation as a Tool of Thought](https://www.jsoftware.com/papers/tot.htm) 的观点,在不被曲解的情况下,不能支持类似 APL 这样的语言的普遍适用。 215 | 216 |   这篇文章指出了记法作为语法设计的基本构成的重要性,旨在批判传统数学在符号系统上的随意设计缺乏关联(和一致性)导致的一些后果。因此,APL 这样的语言相对具有先进性。 217 | 218 |   超出这种范围的应用遇到的问题,不是这篇文章所要表达的内容。这篇文章中的内容也无法解决: 219 | 220 | * 文章提到的应用是对固定规则的应用的举例表示,没有一个元语言性质上体现可编程性的例子。 221 | * 实现上,这些问题自然不需要使用 meta-cirtualar evaluator 这样的抽象机制。一般的编程语言足够应付。 222 | * 文章提供的可供用户编程的表示都是实现意义上的,使用场景有限,并没有解决更一般的抽象缺失。 223 | * 这就像集合论引入的自然数构造的方法不足以取代用户使用数码对自然数进行符号化的表示的需求一样。 224 | * 即便是 APL ,在自然数上也没有要求分解为其它组合子的组合,而是直接提供内建的符号。 225 | * 根本上,这只是具体的实现,而想要表达接口,则需要另行编码。 226 | * 讽刺的是,要原地引入解释器,可能需要和 meta-cirtualar evaluator 同等重量级的计算设施。 227 | * 作者了解在区分上下文的认知,但只是基于已有的应用举例给出了“模式”这样的粗粒度方案,并没有充分讨论其优缺点。 228 | * 实际使用时能发现这种上下文的构造非常薄弱。 229 | * 当然,大多数语言未必擅长解决这样的问题,但允许不依赖现有符号的组合而是直接通过过程定义替换抽象,实用上显然更加有效。 230 | 231 |   关于语法问题,另见[这里](about-syntax.md)。 232 | 233 | -------------------------------------------------------------------------------- /zh-CN/computation-and-physics.md: -------------------------------------------------------------------------------- 1 | # 计算与物理 2 | 3 | # 引言 4 | 5 |   最近发现了一些有趣的联系,在这里简记一下。 6 | 7 | 8 |   这个想法起先出自 Alan Key 关于 Lisp 和 Maxwell 方程组的的评论([[1]](https://www.righto.com/2008/07/maxwells-equations-of-software-examined.html)[[2]](https://www.michaelnielsen.org/ddi/lisp-as-the-maxwells-equations-of-software/))。 9 | 10 | # 对应 11 | 12 | * 计算-物理变化 13 | * 理论计算机科学-理论物理学 14 | * 计算模型-物理理论 15 | * 抽象步骤-时间 16 | * 存储空间-空间 17 | * 程序-物质 18 | * 计算作用(computational effect) -相互作用 19 | * 局域性-定域性 20 | * 全局作用-超距作用 21 | * 副作用-短程作用 22 | * 不变量-守恒量 23 | * 状态-力 24 | * 抽象机语义-力学理论 25 | * 有限状态机-质点力学 26 | * 重写系统语义-电磁学理论 27 | * λ 演算-经典电磁学 28 | * 公理语义-热学理论 29 | * 指称语义-统计物理学理论 30 | * 等式理论-电动力学 31 | * 非平凡 FEXPR 等式理论- Lorentz 协变电动力学 32 | 33 | # 注释 34 | 35 |   副作用的抽象比较微妙。让“纯函数式理论”看着像“宏观系统”并不是本义,但就理论界认识的进度来讲似乎是差不多。不过要描述清楚副作用的理论看来并不需要“量子”这样玄乎的模型,而且想彻底排除以上“宏观”的模拟的唯象理论似乎也不难理解。 36 | 37 |   演绎系统的推理规则原则上符合热力学第二定律,具有单向性。这个意义上,逻辑编程是关于时间反演对称的系统。不过,计算理论仍然缺乏更一般的对称性。虽然不变量被不断地强调,但大部分来自用户自身的设计,也没什么能用 Noether 定理支撑起来的守恒律。 38 | 39 |   Curry-Howard 同构没那么显著的对应。不过这本来就并非完全(存在未对应的偏函数的计算),并且考虑到类型检查作为元语言的程序,在逻辑一侧的可构造性很大程度是显然的。理论上对此的兴趣,也主要是在现有的逻辑理论和现在不甚清楚的计算理论上。这在物理学中同样没有显著的对应。 40 | 41 |   力学是独立于物理学的一级学科。抽象机独立其它计算模型也是传统艺能。 42 | 43 |   就像力在当代物理学不是研究的重心一样,状态通常也不是计算模型设计中首先关心的重点,而是导出的。大概没法有熵力的概念…… 44 | 45 |   关于类型的理论也许可以算经典场论,但代数作用系统(algebriac effect system) 似乎不够格作为“现代”的场论。 46 | 47 |   签名(signature) 也许可以类比实物。不过剩下的影响时空曲率的物质还是算了吧……这部分至少还差一堆副作用没被形式化。 48 | 49 |   垃圾回收不适合足够大的系统(例如现实的分布式系统)。这和超距作用不适合这个宇宙类似。在这个意义上,没有排除 GC 的 vau 演算看起来不能足够类比为相对论性的理论。 50 | 51 | # 其它 52 | 53 |   暂时编不下去了,剩下的以后再说…… 54 | 55 | -------------------------------------------------------------------------------- /zh-CN/contents.md: -------------------------------------------------------------------------------- 1 | # 目录 2 | 3 | 现时包括以下文档。 4 | 5 | ## 术语和文献 6 | 7 | * [术语和文献列表](terms-and-bibliography.md) 8 | 9 | ## 大纲和基础 10 | 11 | * [ISO C/C++ 基础总结](c-cpp-fundamental.md) 12 | * [计算机语言学习导论](introduction-to-learning-computer-languages.md) 13 | * 早期论述参见内文 14 | 15 | ## 概念和纠错 16 | 17 | * [《高质量C++/C编程指南》陷阱](high-quality-c-cpp-programing-guide-trap.md)(另见 [C++ 吧主题](https://tieba.baidu.com/p/1051426693);原贴引用内容地址已失效) 18 | * [_[科普]_​字符串和字符串的长度](string-and-string-length.md)(另见 [C++ 吧主题](https://tieba.baidu.com/p/1758580031)) 19 | * [面向对象和所谓的“面向过程”](OO-and-procedural-oriented-mist.md)(另见 [C++ 吧主题](https://tieba.baidu.com/p/1912906851)) 20 | * [关于 main 函数的原型和返回值](main-function.md)(另见 [C 语言吧主题](https://tieba.baidu.com/p/1969958655)) 21 | * 很久以前就有的话题:[戒除`void main`陋习](https://tieba.baidu.com/p/40625459)…… 22 | * [《与 C++ 相关的一些术语的翻译和问题》评注](cpp-term-translation-comment.md)(另见 [C++ 吧主题](https://tieba.baidu.com/p/2070464220)) 23 | * [C11 & C++11 的赋值相关表达式求值](c11-cpp11-assignment.md)(另见 [C++ 吧主题](https://tieba.baidu.com/p/2091426198)) 24 | * [_[科普]_​变量、全局变量及其它](variables.md)(另见 [C++ 吧主题](https://tieba.baidu.com/p/2126721044)) 25 | * 早期论述: 26 | * [[Terminology]建议慎用“变量”一词](https://tieba.baidu.com/p/1316351174) 27 | * [[菜鸟求教]C语言的范畴里根本就无所谓“全局”“变量”](https://tieba.baidu.com/p/2111194416) 28 | * 更多的是时不时跳出来的一些(大多数是比较 low 的)经: 29 | * [请问什么是全局变量,结构体也算???????????](https://tieba.baidu.com/p/3884720952) 30 | * [求教,怎么把变量转换为常量](https://tieba.baidu.com/p/3962027836) 31 | * [关于异常处理的一些话题](cpp-exceptions.md)(另见 [C++ 吧主题](https://tieba.baidu.com/p/2201116330)) 32 | * [《高质量C++/C编程指南》陷阱 2](high-quality-c-cpp-programing-guide-trap-2.md)(另见 [C++ 吧主题](https://tieba.baidu.com/p/2262386913) 和回复;原文暂不在此全文引用) 33 | * [何勤《轻松学习C程序设计》简评](learning-c-programing-note.md)(另见 [C语言吧背景主题](https://tieba.baidu.com/p/2621752149)) 34 | * [_[高级点的技术交流]_​什么叫语法(syntax)](what-is-syntax.md)(另见 [C语言吧主题](https://tieba.baidu.com/p/3863321148)) 35 | * 虽然是即兴写给[看起来不那么懂语法](https://tieba.baidu.com/p/3835122876)而想要[技术交流](https://tieba.baidu.com/p/3829012515)的,不过实际上先例很多,也算是填了个好几年的坑 36 | * [类型 vs. 类型检查](typing-vs-typechecking.md) 37 | 38 | ## 特性和设计 39 | 40 | * [C & C++广义类型系统缺陷](c-cpp-generic-type-system-defect.md)(另见 [C++ 吧主题](https://tieba.baidu.com/p/2272468675)) 41 | * [转移 vs 复制](move-vs-copy.md)(另见 [C++ 吧主题](https://tieba.baidu.com/p/2403773958)) 42 | * [正确地黑 C](c-wrongs.md)(另见 [C++ 吧主题](https://tieba.baidu.com/p/3190068223)) 43 | * 增补 2 的回复[原贴](https://tieba.baidu.com/p/3221787295) 44 | * [C++ 设计缺陷](cpp-design-defect.md)(另见 [C++ 吧主题](https://tieba.baidu.com/p/3202116449)中的回复) 45 | * [_[科普]_​为什么指针是个糟糕的语言特性](why-is-pointer-awful.md)(另见 [C++ 吧主题](https://tieba.baidu.com/p/3993456389)) 46 | * 早期论述:[[1]](https://tieba.baidu.com/f?ct=335675392&tn=baiduPostBrowser&z=3605563618&sc=64936372503#64936372503) [[2]](https://tieba.baidu.com/f?ct=335675392&tn=baiduPostBrowser&z=3605563618&sc=64963805240#64963805240) [[3]](https://tieba.baidu.com/p/3615289496?pn=2) 47 | * [关于 GC 的意见](about-garbage-collection.md)(另见 [幻の上帝吧主题](https://tieba.baidu.com/p/3171730339)) 48 | * [关于语法](about-syntax.md) 49 | * [决策的哲学](philosophy-of-make-decision.md) 50 | * [关于智能](about-intelligence.md) 51 | 52 | ## 语用和科普 53 | 54 | * [_[科普]_​_[FAQ]_​MinGW vs MinGW-W64及其它](mingw-vs-mingw-v64.md)(另见 [mingw 吧主题](https://tieba.baidu.com/p/3186232510) 和 [C++ 吧主题](https://tieba.baidu.com/p/3186234212)) 55 | * [《基本笔记》系列](elementary-notes/contents.md) 56 | 57 | ## 环境和背景 58 | 59 | * [为什么我不喜欢苹果](why-i-dislike-apple.md) 60 | 61 | -------------------------------------------------------------------------------- /zh-CN/cpp-design-defect.md: -------------------------------------------------------------------------------- 1 | # C++ 设计缺陷 2 | 3 | Created @ 2014-08-01 21:46 rev2 2014-08-01 22:14, rev3 2015-09-15, markdown @ 2015-09-15. 4 | 5 | # 0 不识时务的妥协 6 | 7 | 特别地,兼容(而不是取代) C 。 8 | 9 | # 1 类型系统 10 | 11 | 设计上烂得最广泛的毫无疑问是类型系统——特别地,缺少很多本来该在类型系统内的东西。 12 | 13 | ## 1.1 基本类型 14 | 15 | 基本类型在(所谓的 *fundamental types* ) C 的基础(所谓的 *basic types* )上重写了一遍,有微妙的不同(如宽字符类型),但换汤不换药,缺点差不多一个没少。 16 | 17 | ## 1.2 标准转换 18 | 19 | 继承了 C 的低质量的转换: 20 | 21 | * integer promotion 22 | * array-to-pointer conversion 23 | * function-to-pointer conversion 24 | 25 | ## 1.3 非一等类型(first class types) 26 | 27 | 继承了 C 对数组和函数类型的差别待遇。除了上面的转换,作为在函数参数和返回值也有反直觉的限制。 28 | 29 | ## 1.4 动态类型(dynamic types) 30 | 31 | 添加了动态类型却支持有限,实际上搞出了 [ABI](https://en.wikipedia.org/wiki/Application_binary_interface) 的坑——缺乏公开规范,乃至同一个体系结构和操作系统的二进制代码都没法兼容。 32 | 33 | 题外话,为了描述规则的清晰性 ISO C99 添加了*有效类型(effective type)* ,实际上相当于动态类型,不过没有明确的运行时支持。这反倒不影响实现。 34 | 35 | ## 1.5 对象类型 36 | 37 | ### 1.5.1 语义 38 | 39 | 和 C 一样,C++ 的*对象(object)* 指存储。和 C 不同,C++ 对象的销毁同时可能通过*非平凡析构函数(non-trivial destructor)* 的调用而具有副作用(side effect) 。 40 | 41 | 但事实上,要求存储和要求生存期边界引发副作用是正交的特性。C++ 显然没能处理好这一点:为什么一个只关心析构副作用的对象仍然必须占据存储? 42 | 43 | 此外,类类型是对象类型。不使用特殊规则,基类或数据成员子对象将会占据存储——即便实际上没用到。因此语言不得不使用特别规则进行*空基类优化(empty base optimization)* ——而且对空数据成员的位置还有限制。考虑到许多时候这里的类只是和静态类型系统交互,这种冗余的语义本应是毫无意义的。 44 | 45 | ### 1.5.2 分类学 46 | 47 | C 的对象类型包括 `void` ,而 C++ 把 `void` 独立出来。但是,仍然有一些容易混淆之处:`void*` 是 *object pointer type* ,但不是 pointer-to-object type 。 48 | 49 | ### 1.5.3 布局 50 | 51 | 添加了和 C 不一样的布局,却不提供足够的、明确的、可移植的支持,导致基础设施的失效: 52 | 53 | * 如 `offsetof` 54 | 55 | ISO C++11 提供了*标准布局类(standard-layout class)* 的概念,基本继承于之前的 *POD(plain-old-data) class* 。然而这个概念的定义在之后版本的标准中仍有微妙的变化,并不容易被理解。 56 | 57 | *标准布局类型(standard-layout type)* 的概念构筑在标准布局类之上。其它对象布局就没有附加约束了,如果需要可移植的 ABI ,几乎只能使用标准布局类型。这排除了许多特性,例如具有虚函数而不满足标准布局要求的*多态类(polymorphic class)* 。 58 | 59 | ## 1.6 值类别(value category) 和引用 60 | 61 | 添加了引用类型,想取代 lvalueness (参见 WG21 最早公开的 paper ),后来却坑了。 62 | 63 | 在表达式求值的特殊规则下,引用类型的表达式被视为被引用的实体。这种特殊规则减少了那么一点其它一些语言规则(如 [ADL](https://en.wikipedia.org/wiki/Argument-dependent_name_lookup) )的描述,却实际上使引用类型也不完全是一等实体了。 64 | 65 | * 现在还越搞越复杂,搞出了多种引用类型和 value category ,依然不视为类型 66 | * 并且语义和用例容易被误解,且很多时候难以避免冗余编码 67 | 68 | ## 1.7 添加了参数化多态 69 | 70 | 却对高阶参数化类型(模板模板参数)有莫名其妙的限制导致不可用。 71 | 72 | ## 1.8 添加了实质上的参数化类型 73 | 74 | 却独立于名义类型系统(nominal type system) 之外。 75 | 76 | 概念(concept) 有望部分地改善这点,但无法改变基础设计分裂的现状。 77 | 78 | ## 1.9 添加了类型推导(type deduction) 79 | 80 | 却不足够支持一般意义的类型推断(type inference) ——例如在构造函数上不起作用,需要额外的工厂方法(factory method) 作为惯用法。 81 | 82 | ## 1.10 添加了实质上的重写系统 83 | 84 | 却没有完善的类型系统支持,如: 85 | 86 | * 基于类型的模式匹配 87 | * 自定义重载规则 88 | 89 | # 2 一些深刻的设计失误 90 | 91 | 是差不多所有Algol60直系后裔都存在的功能缺失。 92 | 93 | ## 2.1 同像性(isomorphism) 94 | 95 | 这导致语言中不得不对反射做出特别对待。偏偏到现在还没有。 96 | 97 | ## 2.2 底层高级流程抽象 98 | 99 | 具体点说,能代替 [J operator](https://en.wikipedia.org/wiki/J_operator) 或者 [`call/cc`](https://en.wikipedia.org/wiki/Call-with-current-continuation) 之类的东西。 100 | 101 | 这同时导致诸如 [coroutine](https://en.wikipedia.org/wiki/Coroutine) 等依赖基本控制流的抽象不能在不添加语言特性的前提下被可移植地高效实现;同时也有下面的异常问题。 102 | 103 | ## 2.3 活动记录抽象 104 | 105 | 这导致典型实现及其用户过于依赖特定于体系结构的运行时栈。 106 | 107 | 实际上还同时有栈溢出 UB 这种无解的设定。 108 | 109 | # 3 一些本不适合内建的特性相关问题 110 | 111 | 内建特性在兼容性和扩展性上尤其容易出现问题,特别是存在不够显然的实现的时候。 112 | 113 | ## 3.1 异常不得不被实现为内建的,某种意义上是缺乏上述两者的直接结果——这导致了另外的问题 114 | 115 | * 例如ABI兼容性——同一个体系结构和操作系统的同一版本的同一个实现,使用不同不同异常模型时仍然可能不兼容 116 | * 再如 [WG21/N4049](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4049.pdf) 117 | 118 | ## 3.2 面向对象支持——关键是面向对象本身就没有个相对统一的认识 119 | 120 | * 内建导致之后扩充多分派等需要顾及的太多而最终放弃,远不如 CLOS 之类的库解决方案灵活 121 | * 同样在一些方面导致和加重了ABI问题 122 | 123 | ## 4 阶段(phase) 相关 124 | 125 | 某种意义上是缺乏同像性的副作用。 126 | 127 | ISO C++ 规定的实现阶段数量过多,且过于琐碎。 128 | 129 | 这导致细节难以处理,也难以提高实现质量。 130 | 131 | ## 4.1 预处理被标准化,但是细节比较混乱,导致了很多演进上的问题 132 | 133 | * 像 `#ifdef` 和 `#if defined` 的冗余。 134 | 135 | ## 4.2 缺少可用的模块系统 136 | 137 | ISO C++17 似乎不能保证引入…… 138 | 139 | ## 4.3 存在语言链接支持却缺乏可移植的语言嵌入 140 | 141 | 只有一个 `asm` 还不指望能用。 142 | 143 | ## 5 至于文法/语法嘛…… 144 | 145 | 本来懒得说,不过因为逗到一定境界也顺便提一下好了。 146 | 147 | ## 5.1 兼容 C 的渣声明符中缀语法 148 | 149 | 特别地,C++11 引入 `trailing-return-type` 后也没法放弃旧的语法,并存的结果是增加复杂性。 150 | 151 | ## 5.2 比 C 更多的文法歧义 152 | 153 | 最著名的坑应该就是函数声明优先于初始化对象了。另外还有几个类似的地方。 154 | 155 | ## 5.3 标点问题 156 | 157 | 因为基本源字符集的限制,使用大于(`<`) 和小于(`>`) 的尖括号(angle brackets) 代替了数学上习惯的看起来更扁一点的括号(chevrons) “〈”和“〉”。这导致了极大的解析上的复杂性,同时还需要增设特例规则(记号 `>>` 和记号序列 `> >` 歧义)。 158 | 159 | ## 5.4 少数 C 的语法的不规则延伸 160 | 161 | 如 `new` 表达式。 162 | 163 | 除了单独的消歧义规则的复杂性外,特异的语法规则导致用宏替换实现一些操作更困难,也导致语言规范的复杂(有多少人能分清 type-id 和 new-type-id 呢)。 164 | 165 | -------------------------------------------------------------------------------- /zh-CN/cpp-exceptions.md: -------------------------------------------------------------------------------- 1 | # 关于异常处理的一些话题 2 | 3 | Created @ 2013-03-09, v4 rev 2013-03-27, markdown @ 2015-09-14. 4 | 5 | This is also arranged as a dissertation @ ZJU. 6 | 7 | # 摘要 8 | 9 |   本文对C++语言提供的异常处理机制和异常安全问题进行简要的介绍,并明确关于异常机制在应用中遇到的若干问题及其解决方法,最后讨论异常规范以及C++11对此的改动。 10 | 11 | ## 关键字 12 | 13 | 异常处理,异常安全,RAII,异常中立,异常规范。 14 | 15 | # Abstract 16 | 17 | This article briefly talked about exception handling mechanism provided by C++ and exception safety, and clarifies some problems and solution dealing with error handling. It also concerns exception specification and its evolution in C++11. 18 | 19 | **Keywords**: C++, exception handling, exception safety, RAII, exception neutrality, exception specification 20 | 21 | 22 | # I.绪论:问题边界 23 | 24 |   若无其它说明,本文讨论的异常处理特指C++的一项重要的特性。本文关注它适合的应用范围,但不讨论具体语言实现细节。 25 | 26 |   关于异常处理的基本语法和相关语言特性的使用细节,参见参考资料和其它相关教材。 27 | 28 | # II.背景:异常和异常处理 29 | 30 |   异常在计算机系统中被认为是非正常状态的抽象。异常能够干预程序的正常控制流,它很大程度地影响程序在某种预期条件以外的表现\[1] 。 31 | 32 |   一般地说,当一个异常被处理(handled) 时,程序控制流切换至称为异常处理器(exception handler) 的子例程中。若异常是可继续(continuable) 的,程序可以利用先前保存的信息切换回正常控制流。 33 | 34 |   异常处理被实现为硬件机制和特定的软件构造。前者的例子有 IEEE 浮点异常\[1]、x86架构的双重故障(double fault) 异常\[2];后者如 Windows 结构化异常处理(SEH)\[1] ,以及在多种语言如 C++、Java、Ada 等被内建支持\[1]\[3]。 35 | 36 |   对于软件实现的异常处理机制来说,术语“异常”典型地被作为储存异常条件(exception condition) 的数据结构。异常的传输在这里有普遍的相似性:产生(raise) 或抛出(throw) 一个异常中断正常控制流,直至被捕获(catch) 而处理。若一个异常能在程序的任意部分产生,那么这样的异常是异步(asynchronous) 的,否则是同步(synchronous) 的。 37 | 38 |   由程序设计语言提供支持的异常一般通过编译器生成代码和运行时库动态的检查来实现。编译器需要在确定在特定位置生成代码,这里异常的异步性是受限的。因此,保留某些异常不由语言的实现而是由底层的操作系统或硬件提供支持是合理的。 39 | 40 |   这样,不同层次上的异常处理机制处理不同的异常。例如,在安装 Windows 上的 PC 运行(带有运行时异常支持的)标准 C++ 程序,浮点数异常被硬件处理、影响环境的某些状态后可继续执行;整数除以零或内存访问越界由Windows包装为结构化异常并抛出;通过new操作符分配失败时默认抛出std::bad_alloc异常。被抛出的异常若不被用户显式处理,默认情况下导致程序最终非正常退出。 41 | 42 | # III.C++异常 43 | 44 |   可见不是所有的程序逻辑的异常都被 C++ 处理。标准 C++ 只处理同步异常。 45 | 46 |   一些实现可能支持扩展,如 VC++ 支持非标准关键字 `__try` 、`__except` 等 Windows SEH 特性。对于可移植的 C\++ 程序,不应使用这些特性。 47 | 48 |   通行的做法是,当遇到需要抛出的异常条件时,用一个多态类(polymorphic class)类型的对象(是一种临时对象,称为异常对象)来储存,然后通过throw抛出,到最终需要处理异常处使用和 `try` 块对应的 `catch` 捕获。一般为了方便异常对象的生存期管理,`catch` 指定的类型为异常对象的引用( `const` 在此会被忽略,没有必要)。构造异常对象时一般应避免产生新的异常。 49 | 50 |   `catch(...)` 可以捕获任意类型的异常对象。需要注意的是,在上述带有扩展的实现中,`catch(...)` 可能不安全地捕获到非 C++ 异常\[4]。所以通常避免使用抛出任意类型的异常以及 `catch(...)` ,而使用自己的异常类继承体系,以保证具有可移植性同时能确保适当情况下捕获所有异常\[4]。 51 | 52 |   此外,C++ 可以使用异常规范(exception specification) 来约束一个函数(函数模版)是否接受异常,或接受特定类型的异常,这在本文最后讨论。 53 | 54 | # IV.异常安全 55 | 56 |   异常安全是指在抛出异常后保持可预期的状态。通过 Abrahams 异常安全保证描述异常安全性\[4-6]: 57 | 58 |   基本保证——允许失败操作改变程序状态,但不能有泄漏并且失败操作所影响的对象/模块必须仍然在生存期内并可用,状态必须是可靠的(consistent)(但不是完全可预测的)。 59 | 60 |   强保证——包括事务式的提交/撤销语义:失败操作必须保证关于该操作的对象的程序状态不被改变。 61 | 62 |   无抛出保证——根本不会发生失败操作,该操作不会抛出异常。 63 | 64 |   获得异常安全的一般原则: 65 | 66 |   使用“资源获取初始化”("resource acquisition is initialization")(RAII) 来管理资源的分配——在析构函数中释放资源——自动对象会在异常抛出时析构,释放资源无需用户干预; 67 | 68 |   “从小处做好所有工作,然后保证只使用无异常抛出的操作”从而避免改变程序内部状态,直到能保证整个操作成功; 69 | 70 |   坚持“一个类(或函数),只做一个任务”。 71 | 72 |   参照标准库的策略保证异常安全:一个函数应当总是支持最严格的保证,同时不会对不需要这种保证的用户造成伤害。 73 | 74 |   注意,一些关键函数,如析构函数和去配(deallocation) 函数,必须是无抛出保证的操作,否则在某些条件(具体来说,抛出异常时的栈回退)下无法避免未定义行为,导致程序行为无法预测[3]。 75 | 76 | # V.异常、失败(failure)和错误(error) 77 | 78 |   异常表示非正常,它蕴含了失败——确定在不能完成接口约定的功能且无法在程序中恢复的情形。异常不一定是失败,而失败是异常(虽然有时候为了强调非失败的异常而单独提取出来)。 79 | 80 |   错误(程序设计意义上的,不是指软件的 bug )是程序员需要关注处理的对象,包括失败和违反接口约束但可能恢复的异常。用契约式设计(design by contract) 来概括,这里的接口约束包含三个方面:前置条件(precondition) 、不变量(invariant) 和后置条件(postcondition) \[1]\[7]。 81 | 82 |   错误不应该经常发生。经常发生的状况应该被预期,而不是作为错误处理(error handling)的对象\[7]。 83 | 84 |   对于某些可恢复的异常,C++标准库提供了一些错误恢复例程,如对于默认new失败时\[7]首先会调用new handler,无法恢复时才抛出异常。用std::set_new_hanlder等标准库函数来设置这样的例程[3]。 85 | 86 | # VI.错误处理的方式 87 | 88 |   错误处理是C++异常处理最典型的应用领域。虽然语言并不阻止没有错误的流程中使用异常,但这样可能会导致代码不易读或导致调试时过于容易中断,往往被视为滥用。 89 | 90 |   不使用异常处理的典型错误处理手段是静态存储状态和传递错误码(error code) 。这两种方案都有其局限性。 91 | 92 |   静态存储错误状态(可能存储的就是错误码,如 POSIX 的 `errno` 、Win32 的 `GetLastError` )的局限性很明显; 93 | 94 |   需要静态地分配空间——意味着要么允许错误状态被覆盖(这强迫用户必须在可能发生错误后第一时刻检查错误状态),要么浪费空间(不管可能产生错误的函数是否被调用到都需要); 95 | 96 |   在多线程环境下需要考虑同步及额外开销(若使用线程局部存储,则对实现的要求比较高); 97 | 98 |   几乎是无法重入(reenterable) 的。 99 | 100 |   错误码通过函数参数或返回值传递(如 Win32 的 `GetLastError` ),每一次手动转发只能跨一层函数,和抛出、捕获异常相比,这导致一些明显不利[7]: 101 | 102 |   冗余——显式转发随调用层次增加而递增; 103 | 104 |   混淆正常流程(happy path) 和错误处理流程——往往需要很多代码判断错误码,错误处理代码和其它代码没有清晰的边界; 105 | 106 |   非健壮性——错误码处理实现有误时程序携带错误状态执行,难以确定实现的缺陷会如何暴露; 107 | 108 |   难以确保和检查正确性——若要保证正确,每一处都需检查所有可能的错误是否被处理; 109 | 110 |   耦合——若修改错误码,每一处判断都需要重新检查是否需要修改; 111 | 112 |   无法用于泛型代码——错误码不具备对类型编码的能力,以至于无法区分不同实例类型中相同代码表示的不同错误[7]; 113 | 114 |   无法简洁地跨不同调用层次统一处理错误。 115 | 116 |   特别地,使用返回值传递错误码有如下无法克服的复杂和困难: 117 | 118 |   必须预留返回值给错误——可能占用原本语义上合理的返回值; 119 | 120 |   只能保存极其有限的状态——返回值只有一个; 121 | 122 |   使嵌套调用更复杂——多余的参数需要额外的显式声明; 123 | 124 |   需要小心区分不同返回值的含义; 125 | 126 |   在没有可用的返回值时——构造函数、重载、转换函数中根本无法使用。 127 | 128 |   异常相对于错误码还有其它优势:携带足够的信息,按异常类的继承体系汇总不同错误统一处理,类型安全(不会被莫名其妙地转换)、容易被特定的工具检查等[7]。 129 | 130 |   有观点认为使用C++异常有显著的性能负担,因此应该尽量使用错误码处理错误。这是不正确的。现代编译器可以做到不抛出异常时没有时间开销;相比之下空间开销通常不成问题[7]。但是,应当避免过于频繁地抛出和捕获异常。 131 | 132 |   因此如有可能,应该尽量选择异常作为错误处理机制,除了以下情况: 133 | 134 |   优势无法发挥——错误处理和产生错误的地点非常接近时; 135 | 136 |   造成性能问题——通常是不恰当地使用(例如把频繁出现的状况作为错误)导致的。 137 | 138 | # VII.使用异常进行错误处理的策略 139 | 140 |   这里需要解决的问题是:什么时候抛出/捕获/不抛出也不捕获(使异常隐式地向上层调用者传递,即异常中立)异常? 141 | 142 |   首先是关于未确定为不可恢复的错误时的恢复策略。若确定可恢复时,可以直接原地恢复(例如配置文件不存在,就创建配置文件);否则重新抛出异常,交给上层的调用者处理。 143 | 144 |   然后是关于不可恢复的错误的处理。一是 Sudden Death ,保留足够的信息(如日志)后退出或重启模块;二是保证强异常安全的事务性回滚[7]。 145 | 146 |   回滚的实现主要有两种方式:事先复制可能被回滚的资源的副本,若回滚则使用副本代替已经发生错误的状态,否则丢弃副本;把回滚操作分解为若干具有无异常抛出保证的撤销操作[7]。后者比较节约资源,但受到限制比较大(需要对应的撤销操作都存在)。 147 | 148 |   若有必要,可以重新抛出与捕获的不同的异常(需要不同的异常类型时),或考虑捕获所有异常(在模块边界,不允许抛出异常时)。 149 | 150 |   其它情况下无需特定处理,保持异常中立。 151 | 152 | # VIII.异常规范 153 | 154 |   异常规范指定能从函数被抛出的异常类型,在函数声明(包括定义)中指定。C++11把旧有的异常规范称为动态异常规范,使用throw引导。违反动态异常规范会导致std::unexcepted的调用[3]。 155 | 156 |   动态异常规范有以下缺点\[6]\[8-10]: 157 | 158 |   静态类型的不一致性——它不是类型的一部分(不能出现在typedef中),却限制函数指针赋值和虚函数函数覆盖; 159 | 160 |   运行时强制检查——不对程序员保证所有异常已被处理;阻碍编译器优化代码,影响性能; 161 | 162 |   无法有效地用于泛型代码。 163 | 164 |   在实践中,只有两种异常规范被认为是有用的:什么也不抛出,或者能抛出任何异常。 165 | 166 |   C++11 提供了新关键字 `noexcept` 引导的新的异常规范,并废弃(deprecate) 动态异常规范\[3]\[10],以改善这种状况。`noexcept`只表示是否能保证不抛出异常,而不限定具体的异常类型。违反`noexcept`异常规范会导致`std::terminate`的调用。 167 | 168 |   应该停用动态异常规范。适当使用 `noexcept` 异常规范,以使代码得到更多的优化机会。 169 | 170 | # IX.结论 171 | 172 |   异常是一项 C++ 中的重要特性。使用异常需要注意异常安全,可以通过 RAII 惯用法实现。 173 | 174 |   错误可被作为一类异常对待。异常主要被应用于错误处理,并且一般优于其它方法。使用异常进行错误处理时应特别注意区分错误是否可恢复。 175 | 176 |   异常规范是 C++ 提供的语言特性,用于检查可以抛出的异常的类型。现在应使用 `noexcept` 异常规范而不是过时的动态异常规范。 177 | 178 | # 参考文献 179 | 180 | 181 | 182 | * \[1] Exception handling \[G/OL]. 2013-03-09, 2013-03-10. https://en.wikipedia.org/wiki/Exception_handling 183 | * \[2] Double fault \[G/OL]. 2012-05-02, 2013-03-10. https://en.wikipedia.org/wiki/Double_fault 184 | * \[3] ISO/IEC 14882:2011(E),Information technology — Programming languages — C++ \[S].Geneva, Switzerland,2011. 185 | * \[4] Boost Community. Error and Exception Handling \[A/OL]. https://www.boost.org/community/error_handling.html 186 | * \[5] David Abrahams. Exception Safety in Generic Components [C]. Generic Programming, Proc. of a Dagstuhl Seminar, Lecture Notes on Computer Science. Volume. 1766. https://www.boost.org/community/exception_safety.html 187 | * \[6] Herb Sutter. Exception Safety and Exception Specifications: Are They Worth It? \[J/OL]. Guru of the Week, #82 , 2001-06-30. http://www.gotw.ca/gotw/082.htm 188 | * \[7] 刘未鹏. 错误处理(Error-Handling):为何、何时、如何(rev#2) \[A/OL]. https://blog.csdn.net/pongba/article/details/1815742, 2007-10-08, 2013-03-10. 189 | * \[8] Herb Sutter. A Pragmatic Look at Exception Specifications \[J]. C/C++ Users Journal, 20(7) , 2002-07. http://www.gotw.ca/publications/mill22.htm 190 | * \[9] Herb Sutter. Questions About Exception Specifications \[A/OL]. 2007-01-24, 2013-03-10. https://herbsutter.com/2007/01/24/questions-about-exception-specifications/ 191 | * \[10] Doug Gregor. Deprecating Exception Specifications \[J/OL]. C++ Standards Committee Papers, N3051=10-0041 , 2010-03-12. https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2010/n3051.htm 192 | 193 | 194 | -------------------------------------------------------------------------------- /zh-CN/cpp-term-translation-comment.md: -------------------------------------------------------------------------------- 1 | # 《与C++相关的一些术语的翻译和问题》评注 2 | 3 | Created @ 2012-12-28, markdown @ 2014-11-07. 4 | 5 | 参见[引用原文](https://www.math.pku.edu.cn/teachers/qiuzy/books/cppl/words.htm)。 6 | 7 | 引用著作: 8 | 9 | 裘宗燕译,《C++程序设计语言》,机械工业出版社,2002.7 。 10 | 11 |   原文中讨论若干术语的翻译的观点,包括一些较不流行的译法,可能增加对读者C++理解的困难。遂于此说明,并附个人观点供讨论。 12 | 13 | # 1.关于“侵入式”(intrusive)和“非侵入式”设计 14 | 15 |   目前来看已经可以算是主流术语了,尤其是对于原文讨论的引用计数和容器等。 16 | 17 |   其它可选翻译如 boost-zh-doc 的“介入式”。 18 | 19 | ## 2.1 `public` / `private` 20 | 21 |   有理。所有权(ownership) 目前也是资源管理惯用法中比较明确的概念,而不应对访问权限使用“用”与之混淆。 22 | 23 |   不过适当保留关键字不译更符合语言习惯。 24 | 25 | ## 2.2 `friend` 26 | 27 |   “友元”目前已经是通用译法。 28 | 29 | ## 2.3 constructor 30 | 31 |   可以接受,但并非关键问题。最大的问题是,所谓“构造函数”其实是 constructor 的子集—— constructor 还包括 constructor template (构造模版)。因此像某些 Java 专著那样翻译成“构造器”可能更好点。 32 | 33 | PS:default/copy/move constructor 这些特殊成员倒真是函数。ISO C++ 明确强调了 non-template ,第 12 章的标题就是“special member function”。 34 | 35 | PS2:Java 里面 constructor 不算 method 。为了不跑题过远,constructor 的语义的其它差异暂时略过。 36 | 37 | ## 2.4 `inline` 38 | 39 |   inline 翻译成“在线”看来并不是什么好主意,会被容易误解为“online 的错译”——有这种实例。此外,这里表达的意义并不充分,`inline` 除了可能改变代码生成外,更重要的、决定性的一点是关于 One Definition Rule 的特殊性,跟非 `inline` 修饰的函数不同。 40 | 41 |   基于这点,适当保留关键字不译更合理。 42 | 43 | ## 2.5 garbage collection 44 | 45 |   “废料收集”更合理。 46 | 47 | ## 2.6 smart pointer 48 | 49 |   合理,但需要改变习惯。 50 | 51 | ## 3.1 bind 52 | 53 |   “约束”通常作为 constraint 的翻译,因此不是很合适。 54 | 55 | ## 3.2 override 56 | 57 |   “覆盖”比“重写”更好。后者实质上更强调工程行为,描述的是使用语言的操作而不是任何层次上的语义。 58 | 59 | -------------------------------------------------------------------------------- /zh-CN/elementary-notes/00-about.md: -------------------------------------------------------------------------------- 1 | # 关于本系列 2 | 3 |   本系列文章暂定名为《基本笔记》( *Elementary Notes* ),全名可能叫《关于计算机科学的入门者基本笔记》( *Elementary Notes on Computer Science for Beginners* )之类,不过暂时没有定论;这暂时不是很重要。 4 | 5 |   这些文章的原始的写作动机都一样:为了避免回答一些(非周期性地)重复出现的(“经”)问题的无谓重复劳动。这也是对有些人建议我写书的一个粗浅的回应——尽管我对出版的事项或者盈利的计划并不是很感兴趣。 6 | 7 |   鉴于问题的系统和复杂性,解答也需要相称的体量和体裁,故不使用列表式的 [FAQ](https://zh.wikipedia.org/zh-cn/FAQ) 的类似[某些经典读物](https://zh.wikipedia.org/zh-cn/%E5%85%B3%E4%BA%8E%E6%89%98%E5%8B%92%E5%AF%86%E5%92%8C%E5%93%A5%E7%99%BD%E5%B0%BC%E4%B8%A4%E5%A4%A7%E4%B8%96%E7%95%8C%E4%BD%93%E7%B3%BB%E7%9A%84%E5%AF%B9%E8%AF%9D)的对话体的形式。 8 | 9 |   这个系列构思已久,但一直因为种种原因(主要是懒)和其它文章一起拖着。这也是使用顺序的、自顶向下的方式论述的原因(虽然事实上在我脑子以外并没有什么提纲);当然,也需要注意到提交到版本库的好处是可以可追溯地不断更新补充内容的这个好处。 10 | 11 |   作为[目录](contents.md)外的第一篇文字,本文主要讲述一些不适合自然地放在篇目内的其它内容。 12 | 13 |   以下的体例是工具书类型的说明文字。其它章节请视为正文。 14 | 15 | # 体例说明 16 | 17 |   本章指定体例规则。 18 | 19 |   除非另行指定,适用[版本库指定的全局规则(en-US)](../../Readme.md)。 20 | 21 | ## 编辑标记 22 | 23 |   遵从技术写作的习惯,这里该给一些定义和对应的说明;不过,完整的内容暂时空缺。 24 | 25 | **TODO** 补充内容。 26 | 27 |   其它如有不完善之处使用同上标记。 28 | 29 | **WIP** ←这是一个 WIP(work in progress) 标记,表示正在进行的、有待继续完善的工作。 30 | 31 | **注意** ←这是一个表示注意(notice) 的标记。 32 | 33 | ## 缩进 34 | 35 |   和代码要求的缩进不同,以空格引起使用等宽字体时视觉上对齐为准体现缩进,以摆脱对特定文本处理工具的依赖。 36 | 37 |   编辑标记可以使用全角或半角空格和所属段落保持一致。 38 | 39 | ## 排版体例 40 | 41 |   **强调**以 [Markdown](https://zh.wikipedia.org/zh-cn/Markdown) 的强调实现,视觉效果上为**粗体**。注意,是[粗体](https://zh.wikipedia.org/zh-cn/%E7%B2%97%E9%AB%94),而不是[黑体](https://zh.wikipedia.org/zh-cn/%E9%BB%91%E4%BD%93_%28%E5%AD%97%E4%BD%93%29) 。*斜体* 的使用包括一些次要的强调(这也是在 Markdown 中的本来含义),通常是局部的术语(terminology) 。一些其它的惯例用法,如西文书名,也适用斜体。 42 | 43 |   全角空格的用法同中文书面文法惯例。 44 | 45 |   中西文混排的半角空格应被考虑,如遗漏则可考虑[提交问题(en-US)](https://github.com/FrankHB/pl-docs/issues);但是,需要注意全局规则要求的语言标签(language tag) 的特别的用法:正文的半角括号 () 和左边没有空格,但和右边有一个空格,表示紧接左侧作为专有的术语或者语言代码标注。此外,为了避免 Markdown 渲染的一些兼容性问题,斜体文字周围可能插入冗余半角空格。除有意的对齐外的任何情况下,插入的半角空格不多余连续一个。注意这样的空格的用法和一些其它规则如[《中文文案排版指北》](https://github.com/mzlogin/chinese-copywriting-guidelines/blob/Simplified/README.md)不同。 46 | 47 | ## 参考和引用 48 | 49 |   关于被认为是事实进行论述但并非[原创研究](https://zh.wikipedia.org/zh-cn/%E5%8E%9F%E5%89%B5%E7%A0%94%E7%A9%B6)的材料,通常原则上应给出参考资料来源以佐证出处和可信性。不过因为体裁并非论文或学术专著,这点并不强制——这通常意味着**有可以补充的余地**,但为了阅读或者写作上的体验暂不全部给出。 50 | 51 |   引用的出处可能通过链接给出,而不是文末的标号;当然,链接不一定表示引用,可能只是一个相关的解释;请读者根据上下文内容自行判断。这里,以辨明义项而非精确定义为目的的词条的参考规范性不如正式学术规范一般严格;除非有充分证据显示阻碍真实的[意思表示](https://zh.wikipedia.org/zh-cn/%E6%84%8F%E6%80%9D%E8%A1%A8%E7%A4%BA)或查证的可行性,参考资料恕不保证给出确切的版本。 52 | 53 | ## 外来语 54 | 55 |   有时,一个术语(terminology) 对应的外来语(通常为英文)会在上下文中首次出现时给出以便进一步查证。这里的“首次”的范围指本系列文章(但通常不算作为例子的体例)。基本使用体例为半角括号标注,另见本节文字和排版体例的说明。标注时一般应保证词性对应。在之后的下文中仍可视情况重复标注外来语。 56 | 57 |   外来语可能对应概念(concept) 的原文。作者期望这样有助于更精确的辨义,但恕无法担保理解偏差的存在。在一些并非一一对应的场合——如接口(interface) 和界面(interface) 或者属性(attribute) 和属性(property) ——这样的情形,这种注释性质的原文往往是必要的,但仍有赖于读者的独立思考和正确理解。 58 | 59 |   专名(包括下文中的缩写)须循例使用。如有必要区分,在保持含义准确的前提下,引入注释的术语使用原型单数,且不因为位置(如在标题中)转写其中的字母为大写形式。 60 | 61 | ## 缩写(abbreviation) 和(首字母)缩略语(acronym) 62 | 63 |   首字母缩略语在引入西文外来语的同时给出,如:图形用户界面(GUI, graphical user interface) 。下文优先使用缩略语。 64 | 65 |   其它形式的缩写的使用不在此进行特殊约定,以无歧义和通顺为基本要求。 66 | 67 | # 对预期读者的要求 68 | 69 |   作为基础性的(elementary) 的读物,本系列不对读者的阅读能力要求进行预设,但就现实论,至少得**理解中文**白话;对英文的了解有助于回避特定的一些歧义,不过不是严格意义上必须的。 70 | 71 |   高中程度的知识背景是被建议的,但除了少数技能(阅读理解和命题逻辑相关的知识)外也不是必须的;对本科课程的了解可能会有一些帮助,不过,计算机方面的课程也许正好除外——特别是考虑当这些课程内容掺杂过多流行的谬误而被需要纠正的时候。 72 | 73 |   读者不被要求具有其它学术意义上的训练。不过,作者希望能读者能**独立思考**。 74 | 75 |   因为许多概念的关联性超过文章写作意图,释义恕不能详尽。作者期望读者能适时地**勤于搜索**。作为基础性和识字相差无几的技能,如何实现搜索不是本文关心的问题,尽管可能文章涉及特定话题的更有效率的搜索建议。 76 | 77 |   下文会提到作者对读者的其它一些期望。和要求的区别是,不满足这些期望应不会使阅读基本上成为无效劳动。 78 | 79 | # 价值观(values) 和方法论(methodology) 80 | 81 |   基于可行性,文章不排除任意的预设的对特定的价值的观点和立场。相反,必要时文章内容强调、肯定或否定特定的价值(value) ;这当然不代表读者和作者的需要观念一致。一致的观念是共识(concensus) ,它表示关于某个逻辑陈述具有唯一的真值——要么都肯定,要么都否定。其中,[价值观](https://zh.wikipedia.org/zh-cn/%E5%83%B9%E5%80%BC%E8%A7%80)作为对价值的观念和认识,是普遍的[世界观](https://zh.wikipedia.org/zh-cn/%E4%B8%96%E7%95%8C%E8%A7%82)的精细化。因为文章并非普遍的关于哲学理论的论著,作者避免价值观以外的一般世界观的讨论(如“世界的第一性”这样的[本体论(ontology)](https://zh.wikipedia.org/zh-cn/%E6%9C%AC%E4%BD%93%E8%AE%BA_(%E5%93%B2%E5%AD%A6)) 问题),因为这不必要;这种不必要和作者的价值观不相容。(这种不必要的部分有时候统称为“[意识形态](https://zh.wikipedia.org/zh-cn/%E6%84%8F%E8%AD%98%E5%BD%A2%E6%85%8B)”问题。) 82 | 83 |   文章内容从作者预期和读者之间具有的共识为基础出发,明示或默示地断言一些内容是事实(fact) ,不需另行说明;另一些取决于价值判断([value judgement(en-US)](https://en.wikipedia.org/wiki/Value_judgment)) 。[两者的差异](https://en.wikipedia.org/wiki/Fact–value_distinction)是一个基本的哲学范畴问题,不过这里需要注意核心问题主要就是:是否需另行说明。 84 | 85 |   考虑未形成共识的情况,确认一件事实的判断属于价值判断。但一经承认为共识,则不需价值判断断定其事实地位。因此,一般讨论的价值判断中,排除已确认为共识的事实判断。 86 | 87 |   价值判断的总体(结果)是价值观,也指代价值判断的伦理准则。(注意英文中“价值观”和“价值”都是 value 。)而另外一些认识是和价值范畴独立的;其中普遍的一部分被称为方法论。这种划分蕴含了“是”和“应是”是两类有关但有所区别的问题。读者被期望适时区分这两种情况,即便作者没有强调。 88 | 89 |   因为价值判断的不同体现了立场的差异,从作为共识的事实中剔除这些差异有助于表达和理解问题的效率并影响之后的决策。形式系统(formal system) 中的公理(axiom) 作为“事实”具有类似的价值:避免之后无法休止的废话。 90 | 91 |   基于现实考虑,作为讨论的基础,作者期望读者能充分理解:不得不假设存在一些预设的共识,尽管无法精确地描述它包括哪些。因为,不这样做会遇到现实困难。举个现实的用例:若读者打算不认同作者的声称的所有论述(即不存在共识),那么就不需要继续往下看了,省得浪费时间。这里,“浪费时间”是作者认为的对价值的否定行为,理由是时间作为资源体现了价值。若某个读者的价值判断准则对此无所谓,那么作者自然不需要皇帝不急太监急了。但是,判断读者是不是在乎“浪费时间”,在本文的作者看来就是一种浪费时间(就这个问题来讲这点不需要是共识,但不影响结论),接受判断的必要性引起了逻辑上的不自洽。因此,本文作者不打算推测读者的价值判断的准则;这个系列的文章中显式或隐式的关于“事实”的断言,指的都是(作者认为的)共识的子集。除了预设的共识以及各方都明确断言的对某个判断的肯定(包括读者自行找到第三方证据的情况),形成共识的唯一方法为已经是共识的证明(proof) ,这通过公理、已经是默认的其它前提通过已经是默认的逻辑规则推理得到,其中“已经是默认”意为不言自明。本文作者认为读者应能找到其它证据的情况也包括上文中的体例约定的参考来源。若参考的来源不足使读者难以这样做,可通过修订文章补充;这并非表示论据不足而有必要改变任一方的观点。 92 | 93 |   作者预见这点会造成显而易见的副作用。不只是立场,由于经验的不同,作者认为应该是共识的事实在一些读者来看未必已是共识,而形成不一致,可能造成误会。通过交流互动可能有效地解决分歧,但对渐进编辑的文章很可能低效。基于版本库更新较传统方式更容易缓解这个问题,不过,也只能是一种尽力而为(best effort) 的策略。所以,仅就传播观点这个目的,文章仍不能作为一揽子的替代手段,而仅提供参考,提供的是最需要被复用(而非保证最适合某个特定用例场景)的知识。 94 | 95 |   价值观和方法论在一定程度上有普遍的对偶关系。例如,跳出上面的讨论,一般意义上,作者认为**不应浪费时间**,这是一个价值判断,断言“浪费时间”是和普遍的价值相悖这种价值观;与之对应存在一个表述:**选择避免浪费时间的选项**,这仍然是一个价值判断,但断言的价值有所不同,强调“应该做”的事项,因此是方法论。后者只是给出应该做什么;至于如何判断是否适用这条“应该”,由前者给出。 96 | 97 | # 科普和严谨性 98 | 99 |   这个系列定位是科普,但并不以面向(尽可能广的)大众为主要目的。“科普”和“科研”相对,主要指的是较弱的对规范形式的要求,区别于以实验和产出论文为主的学术研究活动以及实现明确项目目标的工程活动。就普及知识这个方面而言,“科普”的目的有侧重普及知识本身,而不是提升读者的兴趣,或者文化认同上的动机(例如历史上科普曾是传教的附庸)——因为本文预期的读者已有足够的兴趣或者其它理由不得不去尝试正经地理解相关话题。 100 | 101 |   就作者而言,科普的目的和初始动机一致,仍然是为了传播(作者认为正确的)知识,尽量形成共识。可能不那么有趣的一点是,这在实现上的确有些类似传教,但又稍许有些不同——尝试积极地消灭“异端”的错误“教义”的影响,而不是针对“异教徒”。更主要的一点不同是,使读者了解差异(而非接受共识)本身对目的的重要性更加显著。科学这个主题在引起动机的价值判断机制上没有直接关联而显得不那么重要。当然,科学仍然在“计算机科学”的有关讨论中扮演重要角色;这个也是后话。 102 | 103 |   另一个相关话题是“科教”。和科普类似,科教也致力传播科学知识。其不同是,科教力图使读者掌握特定领域的知识为明确目的,且通常会针对性提出考核或评估要求。这不是本系列的主要目的;本系列也不是科教读物。 104 | 105 |   不过仍应当指出,和一些刻板印象不同,科普对知识准确及全面性的要求并不一定弱于科教。虽然为使内容便于接受,科普目的的文献可能会使用较浅显的描述,且有时候更强调娱乐性,但这些并非必要。基于作者的目的,本系列的科普内容可能会显著地偏离这一点,尽管仍然会考虑使用对读者现有知识储备依赖较小的手法。 106 | 107 |   也因为这样,和其它一些通俗读物不同,这个系列的文章不会轻易放弃严谨性;特别地,因为主要的目的包含消除一些流行甚广的误会,也会着重避免引入新的误会以期与这个目的一致。这意味着必要时读者可以自然过渡到专业训练上且避免需要纠正理解偏差的返工——存在这种利益也是预期的共识之一。(不过,一个现实问题是专业训练自身就未必靠谱……这是后话。)严谨本身不是目的,而应是自然的结果。 108 | 109 |   此外值得一提的是,作者认为,即便读者不同意作为论述出发点的作者的多数观点,若理解充分,在一些关键问题上也只能得到相当类似的结论;这种客观性很大程度上受限于逻辑意义上的[理论(en-US)](https://en.wikipedia.org/wiki/Theory_%28mathematical_logic%29)和描述理论的[语言(en-US)](https://en.wikipedia.org/wiki/Formal_language),而这又被讨论的领域内在的特性影响,不以个人好恶为转移。(相反的命题很好理解:只有少数人能懂的学术理论不蕴含没有潜在的价值,在内行之间也如此,比如……极端点的,[Inter-universal Teichmüller](https://en.wikipedia.org/wiki/Inter-universal_Teichmüller_theory)。)因为作者无法为读者的基础知识背景负全责,这里显然需要计较一下可行性的问题。因此,诸如以“×岁小孩都懂”的说理方式来度量科普的有效性的方法,一般来讲,不是值得肯定的价值判断——它实质上偷换了理论为“×岁小孩都懂”的表述形式,这至少一个逻辑谬误。 110 | 111 | # 论科学的精神的要点 112 | 113 |   虽然“科学是什么”在这里讨论的前提并不成熟(我会专门另外论述),不过考虑科学相关的问题有些需要周知的良好品质(尽管实际上未必与科学有关系)并以价值判断而根植于行动中的信条。 114 | 115 |   这里先提两点代表,一是[**还原论**(或**还原主义**)(**reductionism**)](https://zh.wikipedia.org/zh-cn/%E8%BF%98%E5%8E%9F%E8%AE%BA) ,二是[**奥卡姆剃刀**(**Ockham's Razor**)](https://zh.wikipedia.org/zh-cn/%E5%A5%A5%E5%8D%A1%E5%A7%86%E5%89%83%E5%88%80)。简而言之,前者是追求简单形式的原因,后者则要求“如无必要,勿增实体”。在同一适用的前提下,两者基本是方法论,即判断具体什么“应该被还原”或“被剃掉”的问题可以外挂为价值判断而加以利用,于是实践上这些方法是参数化了的。 116 | 117 |   这种规整的性质使之这在一些具体事务(仍可无关科学)能获得普遍的应用。例如,在脱离自然科学背景下,后者可以有[**最小特权原则**(或**最小权限原则**)](https://zh.wikipedia.org/zh-cn/%E6%9C%80%E5%B0%8F%E6%9D%83%E9%99%90%E5%8E%9F%E5%88%99)乃至[极简主义](https://zh.wikipedia.org/zh-cn/%E6%9E%81%E7%AE%80%E4%B8%BB%E4%B9%89)这样的应用。只不过,脱离科学的规则下,坚持或放弃这样的价值判断更取决于主观判断,并没有那么明显的“好”与“不好”的区别,而由具体需求决定——这算是一种实用主义的观点;不过基于有限的资源这个前提,只在“必要”不“简单”是各个领域中通行的默认策略。因为后者的变体过多,在这个系列中我以**最小接口原则(least interface principle)** 统称。 118 | 119 |   另外还有一些在早期自然科学演化中起到关键作用的哲学思想,例如[**怀疑论**(或**怀疑主义**)(**skepticism**)](https://zh.wikipedia.org/zh-cn/%E6%80%80%E7%96%91%E8%AE%BA) ,作者认为在新领域的认知中仍然可以起到一些关键作用。 120 | 121 | # 身份(identity) 、资格(qualification) 和能力(capability) 122 | 123 |   对现实的事件而言,许多情况下我们需要有预期(expectation) 以便之后的决策。这种预期的来源通常难以做到客观“正确”,需要一些补充,基本方法通过调查被分析的对象后评估被关心的一些性质进行。基于资源有限和决策成本,归纳对象的有关性质可以规约为简化的代表来替代这种基本调查方法,牺牲精度(客观准确性)来提升性能(预期判断效率)。根据(一般情况下)性能从高到低、(和直接调查相比)精度从低到高来看,可以归纳为身份、资格和能力。 124 | 125 |   这三者是提取抽象评价之后的结果,而演绎起来(至少相对什么也不提取而直接)十分之简单:比较等价谓词。下面举个实际的例子来说明一般的方法:人力资源部门确定职位的候选人的一部分过程。从现实来说,最靠谱的是“事后诸葛亮”——根据完成的工作的影响进行评价适任性,不过这个代价太高而实际不会完全实践(尽管会有“试用期”这个保底的做法)。在此之前,为了节约岗位资源,能力评价是替代的主要手法。不过,这还是过于具体繁琐,因此通过发布任职资格和考察履历作为补充手段来辅助判断能力评价。尽管不如能力评价来得更符合需要,具体几点任职资格的校验和履历的真实性在有限资源下相对是比较容易直观验证的,也就是操作起来精度可能更低但性能可以更好。而在工作量较大时,学历筛选会进一步取代这些考察手段——更不准确却远远更高效。这些操作的实质是等价谓词比较操作,也就是把候选的数据经过预处理,然后和预期的标准进行匹配,统计候选者是否为符合预期标准的等价类。 126 | 127 |   利用这类方法来权衡评价准确性和性能就操作自身而言无可厚非。不过,因为结果涉及直接的价值判断,结论可能不足以令人信服。这种情况下,引入权威(authority) 消解争议是比较常见的做法。考虑到权威的认定本身就可能由这些简化的判断方式(尤其是身份)引入,自身是否足够具备资格和能力可能也存在争议;但是争议局限在个别点上吸引了多数的注意力,对解决争议的操作来讲,需要的工作量可能是更少的——起码说明争议者会在意这种简化判断的有效性而不是无视之。这种情况下,在某个领域局域权威往往能最终起到作用而平息争议,对权威的身份、资格和能力的认同也随之巩固。不过,这不表示在外部领域不存在争议,因为在外部权威性并不能一定保持权威性。本文作者对此尝试使用平衡的策略,即肯定权威的积极作用,但始终对此保持一定的怀疑,以便减少或避免受到权威但不适用的观点的消极影响。这也是“对预期读者的要求”一节中提到的希望能够独立思考的一个主要原因。 128 | 129 | -------------------------------------------------------------------------------- /zh-CN/elementary-notes/contents.md: -------------------------------------------------------------------------------- 1 | # 基本笔记目录 2 | 3 | 现时包括以下文档。 4 | 5 | * [0 关于本系列](00-about.md) 6 | 7 | -------------------------------------------------------------------------------- /zh-CN/font-rendering.md: -------------------------------------------------------------------------------- 1 | # 字体渲染 2 | 3 |   很久之前就有计划总结关于一些字体渲染(font redering) 相关问题的一些意见,不过因为[话题过大](https://zh.wikipedia.org/zh-cn/%E5%AD%97%E4%BD%93%E6%8E%92%E5%8D%B0%E5%AD%A6),一直坑着。(更大的一个话题是关于广义的“文字”的渲染,至少还包括整段文字表现的排版和复杂样式以及编码上的实现的一摊子烂账,这里先整体排除,只在必要时论及。) 4 | 5 |   这里的问题首先是关于用户角度“设计”的机制问题。具体来说,这样的设计,解决的问题——字体渲染——的预期结果([字形(glyph)](https://zh.wikipedia.org/zh-cn/%E5%AD%97%E5%BD%A) 的可视化),应该在最基本的用户需求出发的意义上遵循的原则。因为一些原因,这是很多用户和设计者都会忽略的浅显问题。考虑到其中包含了和许多其它设计——包括程序语言——一样的问题,因此收录于此。 6 | 7 | # 需求 8 | 9 |   在针对具体问题展开讨论之前,有必要认清楚,字体渲染(确切来讲,单指呈现在预期最终用户眼中的字形的可视化)包含的外延。简单来讲,这里包含两种情形:[印刷(printing)](https://zh.wikipedia.org/zh-cn/%E5%8D%B0%E5%88%B7#%E5%8D%B0%E5%88%B7%E7%9A%84%E5%8E%86%E5%8F%B2) 的效果和在普通显示设备(如显示器或者移动设备的屏幕)上的[光栅化(rasterization)](https://zh.wikipedia.org/zh-cn/%E5%AD%97%E4%BD%93%E5%85%89%E6%A0%85%E5%8C%96) 。 10 | 11 |   这两种场景有一个最重要的共性:有效地传递字形的视觉信息。这其中,最主要的原始需求是传递的文字的内容。这种场景的最终用户是文字的读者:通过接收字形的图像,转换为抽象的图形而解析出文字,以获知文字传达的内容。这种场景下,大多数读者是人类;近年来也有如[光学字符识别](https://zh.wikipedia.org/zh-cn/%E5%85%89%E5%AD%A6%E5%AD%97%E7%AC%A6%E8%AF%86%E5%88%AB)这样模仿人类读者的图像处理技术来自动地完成和人类阅读文字材料时相同的图像处理活动。这种场景下,字形的可视化结果被作为输入的图像;满足这类需求的输入质量的特性,首先是**足够清晰**,其次是**允许足够高效**;其它和文字内容无关的特性并不能直接影响读者成功有效地识别图像。 12 | 13 |   与此相对,把字形作为印刷或光栅图像传输,在现代的技术上并不是什么问题,但这满足的是另一种不同的原始需求。这种需求在一定意义上是和识别图像为文字的上述场景是矛盾的,它要求的首先是作为可视化结果的图像与某个参考模型相比**不失真**。这种模型可能是字体设计者定义的,也可能是更加主观的判断标准;因此,看到具体的模型之前,相对来讲并没有那么绝对意义上正确性。 14 | 15 | # 人的实现策略 16 | 17 |   许多情况下,上述的两种需求在一般的人类读者面前差距并不明显,因为基本上能识别图像并正确转换为文字的读者,也能大体上分辨字形应该具有什么视觉上的结构才能算是满足基本要求——例如,笔画应当呈现清楚而不和其它相似的字体混淆这样不同需求中共同的特性要求。但是,这并不适合所有情况。对诸如字体设计之类的专业场景的要求来讲,读者对图形的处理逻辑可能全然不同。因为若要追求可视化效果和预期的模型尽可能一致,只要有清晰的模型图像甚至抽象的图形,都不需要了解其表达的文字是什么。这种活动本质上不要求读者理解文字是什么(只要能分辨具体不同的字形就可以),是书面语言中立的。 18 | 19 |   因为这些差异的存在,对人来讲,可以有不同的机制高效地处理问题。习惯阅读文字(不用说是专门训练过速读的读者)之后,读者对具体字形的细节差异往往趋向于普遍的不敏感——只要不造成误认别字而造成文字识别错误就可以。更有甚者,对整句和整段落的文字也可以有更激进的特征识别优化,乃至许多情况下存在“汉顺字序不影响阅读”的现象。这种现象也存在于其它书面语的视觉表现上,可以认为是语言中立的。这种优化和处理器前端一次解码多个指令的直接并行有类似之处,但排除单位时间效率问题,实际上更加强大——和文字的语义处理也是流水线并行化的,以至于只要顺序不是错得太厉害到怎么整理都不大可能有意义,人脑总是能够纠错。反之,要求字形和模型匹配的场合,本质上注重的是图像的相似和一致性,并没有经过这层深入图像到文字的识别甚至深入文字的语义的处理,是相当不同的活动。 20 | 21 | # 歪门邪道 22 | 23 |   上述的两个需求和存在不同的合理用例和相当不同的实现,是应当自然地并存的。但是,在如何评价字形光栅化效果的优劣的原则问题上,有的用户不顾这种事实,武断地认为“还原”印刷效果的光栅化总是更好的,凡是和印刷效果不一致的问题,总是光栅化的设计错误。这种观点扼杀了大部分人对不同需求的自然的实现方式,要求不同意见的(应该是这个世界上的大多数的)用户屈从个别的、欠缺理智判断和分析能力的**教条**(据说史蒂夫·乔布斯以设计师的角度鼓吹这种过,暂时没找来源确认),抹杀了用户正常的不同需求表达和选择的自由,因此是一种**歪门邪道**。 24 | 25 |   这里正面点出的问题在逻辑上应当是很清楚的——尊重不同的容易理解存在意义的合理需求的设计,理应是所谓“正确的事”([the-right-thing](https://en.wikipedia.org/wiki/Worse_is_better#The_MIT_approach)) 。但是,许多用户并没有这样完整地分析过问题的来源,也不知道理所应当的问题,单纯以立场出发讨论,而在某些用户群体中引起了不必要的口水战。 26 | 27 |   这个问题的例子集中体现在[一个标题为《为什么 MacType 这种「邪门歪道」的软件会出现并流行?》知乎问题](https://www.zhihu.com/question/49223658/)上。原话“歪门邪道”,出自一个回答: 28 | 29 | ![据信 Micorsoft Windows 字体渲染相关开发者的回答](https://pic2.zhimg.com/f4fe52c0b58e5370f46df27fbc334821_b.png) 30 | 31 |   这里的含义并不清晰,有回答者概括了两点可能的缘由: 32 | 33 | > * 不惜牺牲性能(当然这点性能损耗在今天不算什么),乃至冒着影响稳定性的风险也要 hook Windows 本身的字体渲染机制这种行为,是邪魔歪道; 34 | > * 放着清晰的字体渲染风格不用,偏要去模仿 Mac 的模糊的风格,是邪魔歪道。 35 | 36 |   相比之下,毫无疑问的是,MacType(原名 GDI++ )干预 Windows 字体渲染效果的做法是一种 hack 。这种 hack 的性能在现在来看普遍认为不会造成很大问题,但是稳定性仍然受到质疑。有评论指出“sb2装个mactype直接扑街”,估计指的是 [Surface Book 2](https://zh.wikipedia.org/zh-cn/Surface_Book_2) 。我不理解为什么分辨率 3000×2000 的屏幕需要特别去试 MacType 的效果,使用同款设备也不愿意冒风险验证,这个毕竟还是是主观偏好的问题,暂且按下不表。这里更重要是第二点——隐含“模糊的风格”的不赞同。不过,在展示为什么 MacType 渲染效果(在一些情况下)更好之后,该答主的理由有一些微妙的不同 37 | 38 | > 我又直观地体会到「天呐,mactype 就是正义」\ 39 | > 以上只是一个例子,当有些软件在日常使用中不可代替且在字体显示上也存在不能忍的问题时,用 mactype 改善体验算是成本最小的途径了。对于用户,使用「邪魔歪道」的 mactype 是为了解决更「邪魔歪道」的问题。\ 40 | > **对于用户,使用「邪魔歪道」的 mactype 是为了解决更「邪魔歪道」的问题。**\ 41 | > Win10 RS1 都出了,为什么电脑字体不能像手机一样看的舒服。为什么我需要关心这个程序是 GDI 渲染,那个是 DirectWrite 渲染的,是否需要替换为没有 hinting 的字体,或者 @Belleve 改的字体。对于一名用户,在意这些已经够累了。至于**稳定性**,大概已经忘了这个词吧。 42 | 43 | 44 |   从用户的角度来讲,Microsoft Windows 缺乏技术上本应不难实现(考虑到至少基于 MacType 和其它系统普遍使用的 FreeType 都能实现),而能够改善用户体验的**可配置性**的**功能特性**,而又偏偏不实现这点,的确相当不友好。但是,与其说是歪门邪道,更恰当的评价是,(比起 MacType 这样的魔改都能做出更符合用户预期的方案来讲的)**无能**。虽然本质上只实现有限的方案并给配置制造麻烦同样是不尊重某些用户的需求,但比起**指鹿为马**地以**覆盖其他用户的需求**的形式制造祸端,仍然不是同一个级别的问题。 45 | 46 |   相比之下,另一个回答者更加清楚现实: 47 | 48 | > 以上所说的所有问题,在MS那里,估计永远都不会正式提上议程。因为所有这些都不是Windows广大用户群体所关注的问题,估计放到知乎以外,都没有多少人知道咱们讨论的是什么。所以期待官方解决方案,实在是Naive. 49 | 50 |   按常理想,(即便只是潜意识地)区分清楚两种不同需求解决方案的上述“其他用户”显然占多数。但这更加说明,个别需求蕴含默认策略的不合理性——强求一般用户**总是默认**以图像的方式理解文字的可视化结果,本身就是**不合理**的。 51 | 52 |   作为满足小众用户需求的 MacType ,本身当然不会因此(有资格)成为歪门邪道。这样的小众需求,如果排除对其他用户选择的强迫,仍然可能是合理的,因为: 53 | 54 | * 实现这种需求的操作的合理性已经从可行性和一致性(无矛盾性)中蕴含。即便小众用户因为个别的习惯甚至适应性缺陷(以至于不合时宜的习惯变成了明知问题却仍然被滥用的**瘾**)而选择了对其他多数读者更低效的实现,可以看出这并不妨碍他们解析文字和阅读理解文字的含义。对他们来说,这是合理的选择,这种事实判断不因其他多数人愿意选择的习惯而改变。 55 | * 但厂商是否愿意接受这种习惯为默认情况是另一回事。厂商因为不愿意推出**默认**支持这种方案的配置而被排斥,这样的理由一种**歪门邪道**。 56 | * 是否适应字体可视化效果,很大程度上由用户的眼睛是否能够适应一定时间的阅读有关。不管是作为纯粹的图像还是抽象的图形,用眼疲劳会阻碍阅读是共通的事实。而是否引起视觉疲劳和用眼习惯以及环境相关,这并不是简单能一概而论的问题。 57 | * 以个人经验看,我明确认为字体普遍倾向不模糊的 ClearType 才是可接受的较长时间用眼负担较小的默认方案,因为模糊的文字会潜意识地动用睫状肌试图矫正边缘不够清晰的问题,使眼部负担明显增加而有效缩短可用时间。(这是假性近视患者普遍会加深病情的危险操作,然而散光到一定程度以及真性近视或许就没那么严重的后果。)然而若换上较不常用的深色背景和浅色文字,则 ClearType 和 MacType 风格的眼部不适的差异要小得多——这看来是后者的边缘模糊效果差异也小得多,而不需要动用潜意识的模糊的关系。 58 | * 反之,忽略这个问题而无意义地给用户增加用眼困扰的,不管是出自无知还是迷之自信,都更接近**邪门歪道**。 59 | * 如之前提到的,Microsoft Windows 的在字体渲染的可配置性上的确欠缺功能,不限于默认配置的设计合理与否。而如一些回答提到的,不提供灰度以及 Y 轴子像素反锯齿,在 Windows Vista 后默认强制使用的 ClearType 的问题,确实是实际存在的并且很可能加深视觉疲劳的真实设计缺陷。就从允许自由选择和微调效果使之更好的意义上,MacType 提供一个可被最终用户容易实现的变通,这是合理的。 60 | * ClearType 需要配合字体优化才能实现更合理的效果,这在工程上有更大的工作量而更难实用。但是,技术上讲,全盘依赖不保证完全靠谱 auto hinting 而拒绝手动对字体的优化,这也是**歪门邪道**——特别是这些用户对 CJK 字体设计的繁重工作量没有提供实质贡献的时候。 61 | * 使用 FreeType 这样成熟的方案来实现,技术上自然不是歪门邪道;不过,这和实现的效果如何没直接关系。 62 | * 利用 FreeType 实现一直以来的 Mac 策略的多数 Linux 发行版,个人体验视觉效果并不如 Mac 。MacType 默认配置观感整体还更差(即便无视和不被 MacType 修改渲染效果的界面的不协调问题)。 63 | * 把 FreeType 和 MacType 混淆,则接近直白的**愚蠢**。 64 | * 也应当注意 FreeType 并不只是能够实现这些效果。FreeType 的默认效果和使用的具体 API 和使用的参数及字体都有关。 65 | * 事实上,FreeType 也能光栅化点阵字体(虽然并不怎么直白,以至作为开发者,之前还修复过[误用灰度而无法正确支持 SimSun 点阵的问题](https://github.com/FrankHB/YSLib/blob/f1518054a117619e32d260a11ca30bdef9c2d51b/YFramework/source/YSLib/Service/CharRenderer.cpp#L99))。 66 | * 虽然使用了 C 这样(对一般非“系统”应用来讲尤其)普遍[糟糕](c-wrongs.md)的语言,但项目的代码质量意外地挺不错,模块化和文档的质量都比较高,实现也比 [stb](https://github.com/nothings/stb/blob/master/stb_truetype.h) 之流干净多了(尽管有出现过诸如严格兼容 ANSI C 却仍误用 `_` 起始的保留标识符这样的琐碎问题,不过维护者发现[修正](https://git.savannah.gnu.org/cgit/freetype/freetype2.git/commit/?id=37412ff9f42212bcf4dd29d9762f3c35b5735768)了)。近期 FreeType 的 API 有影响到依赖关系的整体性冗余问题,但这是 [Unicode 造的孽](criticisms-on-UTF-8-everywhere-manifesto.md) 。 67 | 68 |   遗憾的是,该知乎问题下的回答,虽然不乏详尽的比较分析,基本上并没有看穿这里提到的最重要的合理性问题。 69 | 70 |   题外话,我也算是十多年前就用过 GDI++ Helium 版本的老用户了,当时的屏幕条件下就觉得不对劲,调了些配置也觉得原有效果并没有在整体上被明显改善,所以并没有持续使用。(大概是早就近视到无可救药了?)而现在即便 Windows 的渲染仍旧没什么实质改善,在高分辨率屏幕下瑕疵也缩小了很多。由于这类屏幕一旦习惯很容易由奢入俭难,不是没得选择,并不大会使人去使用旧的屏幕。所以大概是没什么机会再刻意体验了罢。 71 | 72 | # 字体配置 73 | 74 |   除了以上问题之外,Windows 的 FontLink 和其它一些方案使用的 fontconfig 的一些不便使用的设计问题……先坑了。 75 | 76 | # 其它相关话题 77 | 78 |   还有其它几个因素也影响阅读渲染后字形的视觉舒适性,和字体渲染问题相关,在此一并讨论。 79 | 80 | ## 字体 81 | 82 |   字体设计毫无疑问地可能影响具体字形可被辨认的难度。 83 | 84 |   排除不实用的符号和艺术字体等,在字号不太大时,一般衬线体比无衬线体略容易辨认,因为衬线体的特征相对离散,更不容易出现挤成一团不易分辨的情形。(当然,小到一定程度什么看起来都费力,另当别论。)这点中文比西文更明显,因为后者字形少得多,而更容易形成“肌肉记忆”绕过分辨字形的开销。而光栅设备分辨率提升有助于呈现细节,则会使衬线体和无衬线体的字形辨认差异普遍减小。所以在分辨率不那么大的设备上,正文字体更适合无衬线体,因为这样通常可以减轻长时间阅读的视觉疲劳。 85 | 86 | ## 光源 87 | 88 |   分辨在非自发光的显示设备上的图像需要外部照明;在完全没有光照时,会字面上地瞎眼,完全无法分辨其中的内容。照明不足同样可能阻碍分辨内容,长时间观看容易造成视觉疲劳。传统的书本在显示特性上属于非自发光设备,也符合这里的描述。 89 | 90 |   自发光设备对环境光亮度的需求较低。不过更容易被忽视的一点是,过强的照明,不论是背光还是环境光,都容易加重视觉疲劳。为了视觉效果,自发光设备的亮度和外部光照条件相适应。正常阅读时,应避免外部过强的光照,以免设备无法提供足够的对比度而不得不依赖眼睛自行适应。过强的环境光(如太阳光)即便经过反射,对眼睛也可能有一定的损害。而即便环境光线不那么强烈,亮度过高也是无益的。 91 | 92 |   另一点与之关联的要素是自发光显示设备或(非自发光设备附带的光源)的刷新率。过低的刷新率可能引起亮度间歇性变化而造成不适。极端情况下,可能出现可明确感知的闪烁;即便没有闪烁,也容易造成视觉疲劳。长时间观看一般需要 60Hz 以上;不过这在主流显示设备中普遍能够达到。 93 | 94 | ## 分辨率 95 | 96 |   在过低分辨率的光栅设备上,字形更加依赖点阵,且可能造成即便显示效果足够清晰也难以辨认的情形,这通常应当被避免。 97 | 98 |   但在更高分辨率的设备上,视觉疲劳可能更加严重。因为显示设备的分辨率会影响眼球的活动状况,这种活动原则上不受到意识的直接支配,但可影响到阅读的舒适性。特别地,当设备的分辨率明显不足以到达视觉极限时,人的视觉成像系统可能会潜意识地分辨图像的细节——即便这对阅读文本毫无必要。 99 | 100 |   解决方法是使用更高的分辨率的设备使之接近或超过视觉极限。经验常识表明,当无论如何分辨也不能获得不同的细节时,眼球应会停止这种加重视觉疲劳的运动。考虑到分辨率等效无限大的非光栅设备(特别是自然光源成像),这种极限的存在是很自然的:正常人的视觉不会试图从无法分辨出像素的来源强行区分出不同的像素。 101 | 102 | ## 背景色 103 | 104 |   先前已经提到,浅色和深色背景有一定差异。对相同字重的相同字形,深色背景和浅色文字的组合相对交换两者的颜色得到的浅色背景和深色文字组合提供的对比度一般更小,所以更依赖加深自重的“模糊”渲染,这是 MacType 在深色背景下表现更清晰的原因。 105 | 106 |   但是,大面积的深色背景使显示设备整体趋近于不发光设备,而更依赖环境光的合理使用。长时间在暗处使用深色背景,容易造成视觉疲劳。如果有可能,最好能有足够强的环境光照。不过,这未必方便(特别是在夜间);此外,这种情况下,和环境的对比度大大增加,深色背景的界面不再适合称为所谓的“夜间模式”。而在环境光线不足的情况下,浅色背景搭配不过于强的自发光设备或背景光源对眼睛更友好。 107 | 108 | -------------------------------------------------------------------------------- /zh-CN/high-quality-c-cpp-programing-guide-trap-2.md: -------------------------------------------------------------------------------- 1 | # 《高质量C++/C编程指南》陷阱 2 2 | 3 | Created @ 2013-04-12, r1 rev 2013-04-12, markdown @ 2015-09-14. 4 | 5 | 答[这篇贴子](https://tieba.baidu.com/p/2262386913)。 6 | 7 | 本文提到的“原文”同样是指《高质量C++/C编程指南》。 8 | 9 | [原文链接](https://web.archive.org/web/20150729130900/https://oss.org.cn/man/develop/c&c++/c/c.htm#_Toc520633988)已经修正。 10 | 11 | (\*)“以实际经验出发”并不和内容有误矛盾。即便实际经验是符合事实的,对于不够有经验的读者还是有相当的危害。 12 | 13 | (\*\*)文章中指出的错误大多是和公认的常识相悖,即便标准能够提供更精确的解释。即便没有标准,错误也不应该出现。 14 | 15 | # 1 16 | 17 | “作者的本意是想说明实际操作中通常的组织代码方式。” 18 | 19 | ↑虽然比较合理的可能的原意能够被有经验的用户通过下文猜出来,但是这个表述无疑是错的。即便不管标准定义,“程序”的概念在C++中是清晰的。声称一个程序通常组织为“两个文件”,是显然的误导。 20 | 21 | ## 1.2.1 22 | 23 | 这里需要补充,实际是否应该使用这种风格取决于具体需要。 24 | 25 | 类模版因为分离定义导致代码过于复杂所以往往不适用,但如果能保证可维护性也不是不能用(如 libstdc++ 拆分的 `.tcc` )。 26 | 27 | 而除了模版外 `inline` 函数也适用。 28 | 29 | 甚至有实用的不用 `inline` 或非模版函数的 header-only 库(如 CImg ——虽然我内部依赖组织得不怎么样,嘛,题外话了)。 30 | 31 | # 3 32 | 33 | ## 3.1 34 | 35 | 命名原则应该足够清晰。 36 | 37 | 需要指出,只强调“直观可以拼读”往往不是最佳实践。当一个“直观”标识符长度过长时就不能使用: 38 | 39 | (1)实现不支持的情况。在ANSI C89只规定较少的标识符长度支持。在C++中这种情况的影响较少。 40 | 41 | (2)大量出现,过长的标识符影响阅读时。一般拟定通用的缩写,在文档(至少注释)说明。 42 | 43 | ## 3.2 44 | 45 | 没记错的话 `namespace` 加入 C++ 是在 90 年代前期提出的,2001 年的主流编译器支持应该不成问题( VC6 也支持)。这不是一个很容易实现错而需要避免使用的特性。 46 | 47 | 考虑到即便是很久以后不少用户对 `namespace` 的使用也有因为各种原因有意无意的回避反而导致放弃使代码更清晰潜力(例如 box2d 和旧版 FLTK ),在这里补充是有必要的。 48 | 49 | # 4.3 50 | 51 | 已修正。 52 | 53 | 链接是BS的个人主页。失效似乎是最近几个月的事情。 54 | 55 | # 5 56 | 57 | 由于历史原因,“常量”的用法比较混乱,具体含义往往需要通过上下文推测。这里对标题的解释不充分显著地增强了误导的可能性。 58 | 59 | 这里是非常基础的内容,不应该有放任明显混淆这样的低级错误。 60 | 61 | “完全”当然不可能。是否“尽量”,也要看需求。例如有时需要用于条件编译的编译时确定的整数值,const在这里无能为力。 62 | 63 | # 6 64 | 65 | 没找到在哪里说“帮助新手区分指针和引用”。出处? 66 | 67 | 帮助新手区分指针和引用完全不必要也不应该使用这种有问题的说法。 68 | 69 | ## 6.3.1 70 | 71 | (1)的确需要补充。 72 | 73 | (2)原文没有指出就是 `assert` 。 74 | 75 | 从契约式设计上来说,用 `assert` 是一种常见的实现,但使用在运行时抛出异常等其它手段有时也是可接受的手法。 76 | 77 | 而 C++11 补充的 `static_assert` (以及之前的模拟)也算,尽管适用范围受限。(题外话,`assert` 就没法 `constexpr` 了,这点大概算是 `C++11` 的缺陷。) 78 | 79 | ## 6.3.2 80 | 81 | 这点确实是错误。已补充修正。 82 | 83 | 不过需要另外指出,“临时变量”的说法是有问题的。C++中的变量是指声明引入的对象或引用,临时变量中的“变量”很多时候无法作为这个含义。从下文来看,是“临时对象”之误。 84 | 85 | 这样,就有一个硬伤。表达式中使用模拟类类型的对象初始化得到的一个非类类型右值,不是临时对象。 86 | 87 | ```cpp 88 | return int(x + y); // 注意:不创建临时对象。 89 | ``` 90 | 91 | 至于后面“已经说过”的问题,重点是刻意区分“值”的意义。这又是一个混乱的概念,尽管严格意义上相对单一。不应该在此处引入增加误导可能。(我很怀疑原作者是否有意识到这点。) 92 | 93 | # 7 94 | 95 | 和本文无关,不过提一下,根据 wikiquote ,这句不是 Bill Gates 说的。 96 | 97 | ## 7.1 98 | 99 | 这点是基础概念问题,和具体实现无关。C 语言也需要讨论类似问题,虽然说法不同。 100 | 101 | # 10.2 102 | 103 | 从 OOD 的角度来看是这样没有错,但注意文章强调的是编码而不是设计上的质量,说的是 OOP 。 104 | 105 | 从 OOD 到 OOP 的映射过程不是单一简单的,毕竟使用的不是建模语言。尤其是对 C++ 来说,不少语义和 OOD 所强调的抽象不相吻合,也不能方便地实现。 106 | 107 | 简化 OOD 到 OOP 的过程或许正是 Java 之流体现价值的重点之一。然而,这样的选择是以牺牲可能实现的灵活性为代价的。而这种灵活性正是 OOP 用户欠缺与需要重视之处。 108 | 109 | 这样的使用可以说是“一种技巧”,但类似地也有其它一些特性,如 class-scope `using` 。C++ 为什么不抛弃这些看起来不符合 OO 方法学的特性?我认为主要并不是兼容性的问题,而是在设计上鼓励语言的用户有权利选择自由灵活的表达方式。 110 | 111 | 就这里的问题而言,`private` 继承的含义是“用……(基类)实现”,即便 OOP 角度上不是典型的组合,同样可以是 OOD 中组合的映射结果。 112 | 113 | -------------------------------------------------------------------------------- /zh-CN/high-quality-c-cpp-programing-guide-trap.md: -------------------------------------------------------------------------------- 1 | # 《高质量C++/C编程指南》陷阱 2 | 3 | Created @ 2011-04-14, recovered rev 2013-04-12, markdown @ 2015-09-14. 4 | 5 | 本文使用 CC-BY 3.0 发布。 6 | 7 | 此文硬伤不少,且相对谭XX的书而言隐晦许多,不建议新手学习。 8 | 9 | 主观的论述,合理的部分,就此略过。原文(点查看)疏漏之处也尽量忍住不吐槽。 10 | 11 | # 第1章 文件结构 12 | 13 | > 每个C++/C程序通常分为两个文件。 14 | 15 | //错误。没有强调翻译单元的概念。 16 | 17 | > 另一个文件用于保存程序的实现(implementation),称为定义(definition)文件。 18 | 19 | //有误。实现不简单等同于定义。例如,类的完整声明也是类的定义,但不是完整的实现。头文件也可以存放其它定义(例如模板和内联函数的实现)。 20 | 21 | ## 1.2 22 | 23 | > ####建议1-2-1#### 24 | > 25 | > 在C++语法中,类的成员函数可以在声明的同时被定义,并且自动成为内联函数。这虽然会带来书写上的方便,但却造成了风格不一致,弊大于利。建议将成员函数的定义与声明分开,不论该函数体有多么小。 26 | 27 | //这条建议一般不适用于类模板。 28 | 29 | ## 1.4 30 | 31 | > 头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。 32 | 33 | //在使用合理的情况下,这基本上是正确的,但头文件可以导致其它方面——像重构(典型情况如重命名一个函数)的复杂化。 34 | 35 | # 第2章 程序的版式 36 | 37 | 这章基本是主观内容。读者需要注意风格是有争议的,其中的“良好风格”或“不良风格”并非公认。 38 | 39 | ## 2.8 40 | 41 | > 很多C++教课书受到Biarne Stroustrup第一本著作的影响,不知不觉地采用了“以数据为中心”的书写方式,并不见得有多少道理。\ 42 | > 我建议读者采用“以行为为中心”的书写方式,即首先考虑类应该提供什么样的函数。这是很多人的经验——“这样做不仅让自己在设计类时思路清晰,而且方便别人阅读。因为用户最关心的是接口,谁愿意先看到一堆私有数据成员!” 43 | 44 | //“Biarne Stroustrup”拼写错误。C++之父的名字是 Bjarne Stroustrup 。有些一厢情愿了:C++ 的类定义中既然允许显式地表示 `private` 成员,就不是纯粹的接口描述方式。以我个人的经验,数据成员写在后面可能会导致顺序阅读代码时需要回溯。当类定义长度较小时这点影响不大,但定义长度比较长的时候(例如 `Loki::Functor` 里的代码)就会需要较频繁地滚屏,影响代码阅读效率。 45 | 46 | # 第3章 命名规则 47 | 48 | 这章也基本是主观内容。 49 | 50 | ## 3.1 51 | 52 | > ####规则3-1-1#### 53 | > 54 | > 标识符应当直观且可以拼读,可望文知意,不必进行“解码”。 55 | 56 | //这点和此文建议使用的匈牙利命名法有一定程度的矛盾。 57 | 58 | > ####规则3-1-3#### 59 | > 60 | > 命名规则尽量与所采用的操作系统或开发工具的风格保持一致。 61 | 62 | //尽管大部分人应该乐于支持这个观点,不过事实上有时候无法实现。例如同时使用标准库和Windows API 风格的代码。这时倒不妨直接约定允许根据上下文选择要使用的命名风格。要点是,应该让人看出某个名称是用哪个风格命名的,而不至于一眼就混淆来源。 63 | 64 | ## 3.2 65 | 66 | > ####规则3-2-7#### 67 | > 68 | > 为了防止某一软件库中的一些标识符和其它软件库中的冲突,可以为各种标识符加上能反映软件性质的前缀。例如三维图形标准OpenGL的所有库函数均以gl开头,所有常量(或宏定义)均以GL开头。 69 | 70 | //在 C++ 中应该考虑是否可以用命名空间代替前缀。 71 | 72 | # 第4章 表达式和基本语句 73 | 74 | > 我真的发觉很多程序员用隐含错误的方式写表达式和基本语句,我自己也犯过类似的错误。 75 | 76 | //作者似乎没搞清楚“错误”一词的含义。 77 | 78 | ## 4.3 79 | 80 | > ####规则4-3-4#### 81 | > 82 | > 不要写成 83 | > 84 | > ```cpp 85 | > if (p== 0) // 容易让人误解p是整型变量 86 | > if (p!= 0) 87 | > ``` 88 | 89 | //事实上,C++ 中的 `NULL` 典型地就是 `int` 字面量 `0` (考虑到成文时间,不提新标准的空指针类型),和 `int` 兼容。以 Bjarne Stroustrup 的观点,这样恰恰会使人误以为 `NULL` 不是整数,因此推荐用 `0` 而不是 `NULL` 。 90 | 91 | ## 4.3.5 92 | 93 | > 或者改写成更加简练的return (condition ? x : y); 94 | 95 | //这里的括号是多余的。 96 | 97 | ## 4.4 98 | 99 | 这节不是语言本身而是涉及语言实现的内容。以现在的观点来看,优化器会可能会在此做一些工作。当然了解一些相关原理大体上还是有益的。 100 | 101 | # 第5章 常量 102 | 103 | > 常量是一种标识符,它的值在运行期间恒定不变。C语言用 #define来定义常量(称为宏常量)。C++ 语言除了 #define外还可以用const来定义常量(称为const常量)。 104 | 105 | //谭XX风格的信口开河。这段引文中逗号或者句号之间的内容,没一个能算得上是正确的。 106 | 107 | ## 5.2 108 | 109 | > ####规则5-2-1#### 110 | > 111 | > 在C++程序中只使用 const常量而不使用宏常量,即const常量完全取代宏常量。 112 | 113 | //这是有问题的。事实上很多情况下 const 只能让编译器被修饰的对象当做只读变量,而非编译期的真正意义的常量进行处理。与 #define 的符号常量(字面量)相比,只读变量受到了一些限制,例如不能作 case 的标号。 114 | 115 | # 第6章 函数设计 116 | 117 | > C语言中,函数的参数和返回值的传递方式有两种:值传递(pass by value)和指针传递(pass by pointer)。 118 | 119 | //错误。形式上,C 语言函数参数只按值传递。所谓的指针传递是按值传递的一种,只是传递参数的类型是指针而已。 120 | 121 | ## 6.1 122 | 123 | > ####规则6-1-1#### 124 | > 125 | > 参数的书写要完整,不要贪图省事只写参数的类型而省略参数名字。如果函数没有参数,则用void填充。 126 | 127 | //不妥当。有时候参数的名称并非有意义,像 int max(int, int); 之类的原型,写了也不会让函数的意义更清楚。此外,并没有指出C函数没有参数时参数列表为 void ,这和省略参数列表(接受任意参数)是不同的。而 C++ 中省略参数列表和 void 参数相同,参数列表 ... 接受不确定个数的参数。 128 | 129 | ## 6.2 130 | 131 | > ####规则6-2-3#### 132 | > 133 | > 不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用return语句返回。 134 | 135 | //在C++中可以不使用此规则而使用异常(在 C 中理论上也可以类似地使用 setjmp/longjmp ,但容易造成语义不明确,实际上基本不用)。 136 | 137 | ## 6.3 138 | 139 | > ####规则6-3-1#### 140 | > 141 | > 在函数体的“入口处”,对参数的有效性进行检查。 142 | 143 | //在函数接口语义明确的情况下并非是必需的。例如 C 标准库 中以及 POSIX 标准中的许多函数。 144 | 145 | > ####规则6-3-2#### 146 | > 147 | > 在函数体的“出口处”,对return语句的正确性和效率进行检查。 148 | 149 | //同样不是必需的。此外检查可能损失效率。 150 | 151 | >要搞清楚返回的究竟是“值”、“指针”还是“引用”。 152 | 153 | //注意返回对象的语义,但是刻意区分“值”和“指针”是不必要的,严格上是错误的——它们根本就不是可以比较的一类概念。 154 | 155 | > ####建议6-4-2#### 156 | > 157 | > 函数体的规模要小,尽量控制在50行代码之内。 158 | 159 | //尽管为数不多,有些特殊情况,如编译器的某些分析程序,是明显的反例。 160 | 161 | # 第7章 内存管理 162 | 163 | > “640Kought to be enough for everybody\ 164 | > —Bill Gates 1981” 165 | 166 | //和本章主题无关。 167 | 168 | ## 7.1 169 | 170 | > 内存分配方式有三种 171 | 172 | //有误。内存分配具体方式由实现决定,语言只限制存储类。 173 | 174 | ## 7.2 175 | 176 | 漏了重复释放内存的错误(这通常会引起程序崩溃)。 177 | 178 | > ####规则7-2-1#### 179 | > 180 | > 用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。 181 | 182 | //对 C++ 而言是错误的。ISO C++ 关于内存分配失败的默认行为是抛出 std::bad_alloc 异常。如果要使分配失败不抛出异常,使用 nothrow 版本,或者设置实现相关的编译选项。 183 | 184 | > ####规则7-2-5#### 185 | > 186 | > 用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。 187 | 188 | //不一定必要。例如指针是自动变量,在退出所在的块作用域被自动释放时。 189 | 190 | ## 7.3.1 191 | 192 | > 该语句企图修改常量字符串的内容而导致运行错误 193 | 194 | //有误。C 语言中字符串字面量(具有数组类型)未必是常量。当然还是应该避免修改字符串字面量,这是未定义行为。 195 | 196 | ## 7.9 197 | 198 | > 为new和malloc设置异常处理函数。例如VisualC++可以用_set_new_hander函数为new设置用户自己定义的异常处理函数,也可以让malloc享用与new相同的异常处理函数。 199 | 200 | //应该补充的是,标准库有 std::set_new_handler 。 201 | 202 | # 第8章 C++函数的高级特性 203 | 204 | > const与virtual机制仅用于类的成员函数。 205 | 206 | //应该强调“非静态”成员函数,否则就是错误的。 207 | 208 | (虽然似乎没有更多明显的错误,不过遗漏的地方一堆,坚决不吐槽……= =) 209 | 210 | # 第9章 类的构造函数、析构函数与赋值函数 211 | 212 | ## 9.1 213 | 214 | > Stroustrup的命名方法既简单又合理:让构造函数、析构函数与类同名,由于析构函数的目的与构造函数的相反,就加前缀‘~’以示区别。 215 | 216 | //错误。构造函数和析构函数是无名的(这个细节决定了一些语言特性的限制,例如不能 using 声明基类的构造函数),看起来名称和类名相同的调用方式其实是语法的限制。 217 | 218 | > 非内部数据类型的成员对象应当采用第一种方式初始化,以获取更高的效率。 219 | 220 | //效率在这里倒是次要的(很容易被编译器优化掉),重要的是语义。另外,初始化列表的一些行为是受到特殊限制的,例如基类子对象和成员的初始化顺序和异常。 221 | 222 | ## 9.7 223 | 224 | (仍然不想吐槽漏掉 swap & copy 这样高效可靠的方法而在 `operator=` 实现里分配内存……) 225 | 226 | # 第10章 类的继承与组合 227 | 228 | 首先标题有点问题。类可以继承,组合的应该是类的实例而非本身。 229 | 230 | > 如果将对象比作房子,那么类就是房子的设计图纸。所以面向对象设计的重点是类的设计,而不是对象的设计。 231 | 232 | //因果关系不成立。而且观点是有问题的,仅适于经典的 class-based OOD 。 233 | 234 | ## 10.2 235 | 236 | > ####规则10-2-1#### 237 | > 238 | > 若在逻辑上A是B的“一部分”(a partof),则不允许B从A派生,而是要用A和其它东西组合出B。 239 | 240 | //错误。尽管一般组合能够胜任这项工作,但并非绝对。可以使用private继承完成相同的任务,并且提供覆盖成员函数的特性。这是组合无法完成的。 241 | 242 | # 第11章 其它编程经验 243 | 244 | ## 11.1.3 245 | 246 | > 如果在编写const成员函数时,不慎修改了数据成员,或者调用了其它非const成员函数,编译器将指出错误 247 | 248 | //错误:漏了 `mutable` 修饰的成员这个特例。 249 | 250 | > ####建议11-3-8#### 251 | > 252 | > 避免编写技巧性很高代码。 253 | 254 | //不应该逃避。技巧性高不意味着难以理解;即使难以理解,也可以通过注释弥补。当然,前提是技巧要使用得合理。 255 | 256 | > ####建议11-3-13#### 257 | > 258 | > 把编译器的选择项设置为最严格状态。 259 | 260 | //有时候现实不允许,例如考虑已有代码的兼容性,需要配置时会带来额外的复杂性。 261 | 262 | # 附录C :C++/C试题的答案与评分标准 263 | 264 | > 标准答案示例:\ 265 | > const float EPSINON = 0.00001;\ 266 | > if ((x >= - EPSINON) && (x <= EPSINON) 267 | 268 | //如果是“标准”的,为什么不用标准库的 ``/`` 的 `FLT_EPSILON` 而自己重复发明轮子? 269 | 270 | > 三、简答题(25分)\ 271 | > 1、头文件中的ifndef/define/endif 干什么用?(5分)\ 272 | > 答:防止该头文件被重复引用。 273 | 274 | //错误。条件编译显然不只用于给头文件增加 header guard 。 275 | 276 | > 四、有关内存的思考题(每小题5分,共20分)\ 277 | > 答:程序崩溃。\ 278 | > 因为GetMemory并不能传递动态内存,\ 279 | > Test函数中的 str一直都是 NULL。\ 280 | > strcpy(str, "hello world");将使程序崩溃。 281 | 282 | //错误。未定义行为不等同于程序崩溃,尽管很可能是这样。 283 | 284 | > 六、编写类String的构造函数、析构函数和赋值函数(25分)\ 285 | > `String& String::operate =(const String &other)` 286 | 287 | //关键字 `operator` 拼写错误。实现冗余就不说了。 288 | 289 | -------------------------------------------------------------------------------- /zh-CN/learning-c-programing-note.md: -------------------------------------------------------------------------------- 1 | # 何勤《轻松学习C程序设计》简评 2 | 3 | Created @ 2013-08-31, rev v1 2013-08-31, markdown @ 2015-09-15. 4 | 5 | 版权声明:本文使用 CC-BY-SA 3.0 发表。 6 | 7 |   应作者要求,阅《轻松学习C程序设计——揭开计算机与程序设计的奥秘(修订版)》(资源通过网络找到),评论如下。 8 | 9 | (此处找到的材料的节标号可能有错乱,按页面顺序评论。因此也不保证分节评论。) 10 | 11 | # 1 计算机的重要性和基本工作原理 12 | 13 | 作者使用“理想厨房系统”作为计算机系统的比喻。有以下一些问题: 14 | 15 |   难以估计对于初学者来说这个比喻是否得当,有助于真正理解。 16 | 17 |   厨房中突兀地出现了“地址传送带”“控制传送带”“IR”“PC”“R0”等和喻体不直接相关的生造抽象。这些抽象是否真容易理解?后面虽然有比较详细的说明,但细节并不十分直观且篇幅不小,可能对读者造成负担。 18 | 19 |   语言风格问题。虽然总体比较清晰,但一些名词并不十分浅显易懂(相反容易觉得很“专业”——某种意义上事实的确如此)。如“取指周期”——具体什么意思,并没有在此直接解释。 20 | 21 | 《理想厨房系统与计算机系统术语对照表》中: 22 | 23 | > 内存(又称为主存,包含很多大小相等的基本存储单元) 24 | 25 | ——错误。 26 | 27 |   虽然通常说的内存往往是指主存,但实际上是两回事。 28 | 29 |   在论述存储的体系结构时,习惯上“内存”指的是联机存储,和脱机存储即“外存”对立。 30 | 31 |   很容易找到反例,如智能手机等设备常见的 Flash ROM 属于内存,但它不是主存。这些设备的主存使用 RAM 。 32 | 33 |   此外,“内存”作为英文 memory 一词的翻译,也可以指一般的存储,没有特别的“内”的意思。只不过通常在 PC 等设备上程序必要的主要存储就是联机 RAM 主存抽象得到的,主存、内存和 RAM 三个概念有时候就会被混用。(后文有区分 RAM 和内存,但并没有完全论述清楚。) 34 | 35 | > 一个字节(即8位) 36 | 37 | ——一个字节即 8 位也只是通常情况,不总是成立。 38 | 39 |   下文 40 | 41 | > 字节:一个8位的二进制的位串,就构成了一个字节(Byte) 42 | 43 | ——在这个意义上显然是错误的。 44 | 45 |   正式地说,在体系结构中的字节和八个位组成的八元组/八位组(octet) 是两个相对独立的概念。只是通常一个字节是 8 位容易导致一些错觉。 46 | 47 |   习惯上,作为存储容量的计量单位,一个字节被默认为 8 位组成,ISO/IEC 80000-13 规范化了字节的这种含义。但是,更严格的场合中,如通信、编码等规范化时,会严格区分两个概念。 48 | 49 |   特别地,C 语言明确允许一个字节大于 8 位。考虑到 C 语言是本书讨论的重点,在这里混淆这两个概念是不合适的。 50 | 51 | 二进制数介绍比较详尽,但较复杂,初学者可能比较难接受。 52 | 53 | > 操作系统又被称为系统软件 54 | 55 | ——系统软件是个比较笼统的概念,但显然不止包括操作系统。 56 | 57 | > (称为可执行程序,程序文件名的扩展名为.exe或.com) 58 | 59 | ——显然错误。文件扩展名只是特定环境(如操作系统)下的约定。文件是否表示可执行的程序,取决于文件的内容以及环境是否允许。 60 | 61 | 本章内容比较多,可能缺乏一些典型的简单抽象,如冯·诺依曼结构只是一笔带过。对于读者而言,一口气理解大量术语和比喻(是否一定有助于理解还有疑问),很可能比记忆经典的简单抽象更困难,且更难以保证理解内容的准确性。 62 | 63 | > 不过,Lisp,Forth这些函数式语言与以上所列这些命令型或面向对象型语言有较大的区别 64 | 65 | —— Forth 不是典型的函数式语言,相反,它的主要范型是指令式/命令式(imperative) 的。 66 | 67 | # 2 C语言的基本概念 68 | 69 | ## 2.1.2 C语言程序的一级构成成分——函数 70 | 71 | “一级构成”是无稽之谈。 72 | 73 | > 参照本书末附录A的上机指导,在Windows操作系统环境下,使用VC++6.0编译并运行以下C语言程序(虽然这是一个C++语言的编译器,但该编译器也很适用于对C语言的源程序进行编译和调试。只不过要记住:C源程序的文件名一定要以“. c” 作为扩展名。注意:此处扩展名用的“c”是小写,不能用大写。如果你忘记了加上这个扩展名,该编译器就会将你的程序当作C++的源程序进行编译。 74 | 75 |   在扩展名上又一次的低级错误。注意 Windows 原生的文件系统( FAT 和 NTFS 等)上虽然可记录文件名中字母的大小写,但是把仅有大小写差异的文件名视为相同文件的文件名。所以 VC++ 区分大小写是完全没有必要的。 76 | 77 |   通常类 UNIX 环境的文件系统严格区分文件名中字母的大小写,使用的编译器会 .C 作为 C++ 程序的扩展名。 78 | 79 | > 一个C语言源程序类似于某个特殊的公司,main()函数的角色类似于公司的总经理(该公司的特殊性在与:每个员工所负责的工作都是互不相同的) 80 | 81 | ——尽管下文有表指出大致的对应关系,但缺乏直观性,略牵强。 82 | 83 | ## 2.2 C语言程序的二级构成成分——定义、语句、注释和预处理命令 84 | 85 | 标题即体现抽象层次混乱。事实上这四个概念中没有任何两个是并列的。而且,从严格规定的翻译阶段(phase of translation) 来看,注释和预处理指令比上文所谓的“一级”成分函数更高。因此这是显然错误的。 86 | 87 | > 其中的PI是一个符号常量 88 | 89 | ——符号常量是个现时不常用的老旧说法,不是 C 的正式概念,且容易引起混淆。(什么是“常量”?) 90 | 91 | > 这是一条本章将要重点讲解的赋值语句。 92 | 93 | —— C 并没有单独的赋值语句。 94 | 95 | 使用了 C99 引入的单行注释“//”,但和本书其它部分强调 C89 和 C99 的区别不同,这里没有提到 C99 。(虽然 VC++ 到 2013 年都没完全支持 C99 ,不过这个倒是有扩展支持了。) 96 | 97 |   此外,关于标准的版本,正确说法是 ANSI C89 的正文被接受为 ISO C90 ,而 ISO C99 在 2000 年被接受为 ANSI C 标准。之后标准一般直接称为 ISO C 。本书所谓的 ANSI C99 的提法不合适。 98 | 99 | > C语言程序的二级主要构成成分,分为两大类:定义序列和语句序列。在函数体中,定义序列在前,语句序列在后 100 | 101 | ——显然错误,并且无视了声明的重要性。 102 | 103 | > 高级语言源程序中的定义,是用来定义变量和定义(用户自定义的)类型的 104 | 105 | ——漏了函数,非常不妥。下面 106 | 107 | > 每一条定义都要以分号结束 108 | 109 | 显然也是错的——考虑函数定义。 110 | 111 | > 高级语言源程序中的语句,其实就是用来告诉编译程序:我们想要计算机对于源程序中以变量或常量形式出现的数据,执行什么样的运算(算术运算还是逻辑运算等等)和操作(取数、存数、输入、输出);或者,我们想要计算机根据哪个表达式的计算结果,去选择下一条要执行的语句(这句话,你或许要学了选择结构这一章以后,才能真正懂得)。 112 | 113 |   这一段再次暴露了抽象的混乱。实际上,在 C 这个层次上,运算完全可以归类为操作的一种,并不需要区分存取和算术操作——而这些操作的语义蕴含于表达式(不是语句)的求值中。 114 | 115 |   而 ISO C 使用的方法是定义一个抽象机,操作表现为这个抽象机的行为。存取可能是表达式求值包含的副作用(具体地,对外部环境的 I/O 操作和向对象存储值一定是副作用,volatile 左值的读取也是副作用)。 116 | 117 | > C语言源程序的二级次要构成成分是:注释、编译预处理命令和声明(只是对在别处出现的定义,起着辅助说明作用。请参见函数一章) 118 | 119 | ——错误。直接无视了声明作为定义,相当于篡改了“声明”的概念。 120 | 121 | > 命令型(或称为过程型)高级语言源程序的本质 122 | 123 | ——命令型和过程型不是一回事。关于这点在[《面向对象和所谓的“面向过程”》](https://tieba.baidu.com/p/1912906851)已经有过澄清。 124 | 125 | > 与大多数其他高级语言不一样,在编译C源程序之前,都必须事先运行一个编译预处理程序 126 | 127 | ——错误,不在所有情况下适用。尽管完整的翻译阶段包含预处理,但是预处理后的中间程序可不需要再次预处理,且仍然可以是 C 源程序。 128 | 129 | > 对源程序进行一些(通常是少量的)辅助性的插入、替换和编辑工作。 130 | 131 | ——实际情况通常很不“少量”。可以观察 GCC 等常用实现的预处理后的程序,比较一下和原始代码的大小。 132 | 133 | > 但也有一些编译预处理命令——比如条件编译命令——是可以书写在函数体内部的 134 | 135 | ——这里的说法比较含糊。预处理本身不禁止指令和函数的顺序,事实上通常预处理器的实现无视函数。尽管不是常规做法,`#include` 等指令写在函数体内部也可行。 136 | 137 | ## 2.1.6 C语言源程序的编写、编译、链接和调试过程(参见附录A) 138 | 139 | > 这里的流程和 VC++6 实际所做的不一样。例如,`nmake` 的调用被无视了。虽然可以理解这样描述是为了方便新手阅读,但接下来关于编译器的行为也是很含糊的。 140 | 141 |   之前本书有提到“编译程序(又称为编译器)”——实际上这个说法是不严格的。典型的实现中,供用户通过命令行接口调用有若干个程序(可执行程序或库),它们可以总称为编译器。其中一个程序称为编译器驱动程序(compiler driver) ,负责调用其它程序完成预处理等,有时候也被称为编译器。 142 | 143 |   对于 VC++ 来说,C/C++ 编译器的驱动程序一般是 cl.exe 。VC++ 中 cl 调用了附带的动态链接库完成翻译的不同阶段,一般笼统地称为编译——而链接则是 link.exe 完成的。 144 | 145 |   对于 GCC 来说,由于按 *NIX 传统倾向于直接调用分别调用各个静态链接的可执行程序而不是动态库,这点更加明显:可以很容易观察到占用最多时间的往往是实际执行代码生成工作的程序(例如 MinGW 和 Cygwin 下的可执行文件名是 cc1plus.exe )。 146 | 147 | > 不走弯路,通过一本书就能真正掌握编程的基本思路和技巧,就是最短最快的捷径 148 | 149 | ——这个有 [Peter Norvig 的吐槽](https://tieba.baidu.com/p/2563465238)大概就够了。 150 | 151 | ## 2.3 C语言源程序的正文部分 152 | 153 | 所谓的“正文部分”仍然是生造的概念。 154 | 155 | ## 2.4 C语言的字符集 156 | 157 | > C语言源程序的全部正文部分,都只能够使用如下所列举的字符来构成 158 | 159 | ——其它地方说的好好的 C99 呢? 160 | 161 |   此外,标题党。这里所说的实际上是基本源字符集(basic source character set) 。明明标题是“ C 语言的字符集”,另一个重要的基本执行字符集(basic execution character set) 却一点不提。 162 | 163 |   全角和半角也有乱掉的情况(下略)。 164 | 165 | ## 2.5 标识符 166 | 167 | > 在高级程序设计语言中,我们通常用标识符来命名,我们想要用计算机进行加工的其数值可以变化的数据——变量(→)、不可变化的一部分数据——符号常量 168 | 169 | ——又来了个“符号常量”的混沌说法。 170 | 171 | ## 2.6 关键字 172 | 173 | > “关键字”有的教科书又称为“保留字” 174 | 175 | ——这里补充一下,对于 C 来说问题不大,但有些语言后者范围更大:如 Java 的 `goto` 是保留字但不是有意义的关键字;再如 C++ 的 `and` 等 alternative token ,尽管不是正式说法,也可被称为保留字。 176 | 177 | > 切记:不要将关键字作为普通的标识符来定义和使用 178 | 179 | 意思容易理解,但是说法不严谨(考虑到后面提到了“分隔符”之类的词法问题,这里的漏洞更加严重)。 180 | 181 |   在 C 语言中,术语“标识符(identifier) ”出现在两个地方:一种是作为预处理记号(preprocessing-token) ,此处只有标识符没有关键字;另一种是记号(token) ,其中包括关键字和标识符等。 182 | 183 |   即预处理之前的标识符被预处理后分化为关键字、标识符和其它记号。 184 | 185 | ## 2.7 分隔符 186 | 187 | 标题的分类是胡扯。无论是 seperator 还是 delimiter 都不像。 188 | 189 | > C语言字符集中的空格,逗号,回车/换行(ASCII码为13)这三个字符在源程序中起着分隔的作用 190 | 191 | ——词法和语法混起来讲先不论,制表符的存在感呢? 192 | 193 | > 在同类项之间作分隔:要用逗号,空格则可加可不加 194 | 195 | ——什么叫“同类项”?后面有例子,但是比较笼统——似乎只是说了变量名之间算同类项,但又无法让读者知道是不是有其它适用的外延。 196 | 197 | ## 2.8 常量 198 | 199 | > “A.整型常量 567,-425 ,0 等,是没有小数分量的数值” 200 | 201 | ——整型常量?是指整数常量(integer-constant) ?负号是什么情况(看来又是谭×流么)? 202 | 203 | > B.实型(浮点型)常量 204 | 205 | ——“实型”看来又是谭×流的说法。 206 | 207 | 浮点数十六进制表示和二进制阶码果然没存在感。 208 | 209 | > 而是通过采用编译预处理命令中的宏定义的符号常量(→)来处理此事。即用标识符来命名的常量 210 | 211 | ——完全胡扯。常量(constant) 的语法里根本就没有这号玩意儿。 212 | 213 | ## 2.9 变量 214 | 215 | > 比如register int num; 编译器就很有可能将变量num的存储单元安排在CPU的寄存器中) 216 | 217 | ——现代的编译器比较少理会 register 关键字,因为往往编译器比用户更清楚到底是不是存进寄存器里比较高效( C 还好点,C++11 直接 deprecated 掉了)。 218 | 219 | > 给要使用的变量起名字,正式的术语称为定义变量 220 | 221 | ——胡扯。“`extern int i;`”显然可以不是定义,但它就是取了个名字。 222 | 223 | > 对于简单类型的变量 224 | 225 | ——没说清什么是“简单类型”,大概是想说声明符可以放在要声明的标识符一侧,不用顾及函数或数组的声明符。当然 C 在这里是比较坑。 226 | 227 | > 变量名一般要用标识符来命名 228 | 229 | ——难道还可以不用标识符命名? 230 | 231 | > 任何一个int 型的整型变量,在VisualC++ 6.0编译环境下都被编译程序分配了4个字节的内存空间作为存储单元;在Turbo C 2.0编译环境下被分配了2个字节的内存空间作为存储单元。 232 | 233 | ——这种事无巨细的说法是很糊弄的风格。凭什么就不能是 8 个字节?再者,如果整个优化掉了呢? 234 | 235 | > 在程序运行中,以实数值形式(即有小数分量)出现的变化着的数值(比如34.1,-678.34等) 236 | 237 | ——无理数算不算有小数分量的实数? 238 | 239 | > 单精度浮点型变量的有效位数是十进制的7位 240 | 241 | ——并非刚好 7 位。 242 | 243 | > 任何字符型的变量,在内存空间都被编译程序分配了一个字节的内存存储单元,用来存放该变量所对应字符的ASCII码(大多其他高级语言也都是这样) 244 | 245 | ——又是常见错误。 246 | 247 |   上面提到过了,C 有基本执行字符集。这决定了运行时存储的整数和所表示的字符的关联。 248 | 249 |   更重要的一点是,无论是基本源字符集还是基本执行字符集,都只是说要至少包含哪些字符,没明确具体实现为什么字符集什么编码。 250 | 251 |    ASCII 只是最常见的实现。一个不兼容的 ASCII 的例子是 EBCDIC 。 252 | 253 | -------------------------------------------------------------------------------- /zh-CN/mingw-vs-mingw-v64.md: -------------------------------------------------------------------------------- 1 | # \[科普]\[FAQ]MinGW vs MinGW-W64及其它 2 | 3 |   部分参照[备忘录原文](https://bitbucket.org/FrankHB/yslib/src/50c3e6344a5a24b2382ce3398065f2197c2bd57e/doc/Workflow.Annual2014.txt?at=master&fileviewer=file-view-default#Workflow.Annual2014.txt-452)。 4 | 5 |   试试问答体。首先得绕个远路,从 Win32 开始说起,否则之后容易乱…… 6 | 7 | # 什么是 Win32 ? 8 | 9 |   嘛,32 自然是指 32 位了?不一定。 10 | 11 |   正式地说,Win32 主要是指跑在 Windows NT 内核上的 Win32 子系统。现在 x64 的 Windows 上的大部分程序也是跑在这个子系统上的,`system32` 目录也没叫成 system64 。 12 | 13 |   尽管 32 的语源的确来自于“32 位”。 14 | 15 | # 那么为什么还有 Win64 ? 16 | 17 |   这倒可以肯定,这里的 64 是指 64 位目标平台,因为没有上面的那种歧义。 18 | 19 |   有一点值得注意,在 MSVC 中,32 位环境(当然是说跑的 Intel 兼容CPU的PC)预定义宏 `_WIN32` ,但 64 位环境同时预定义了 `_WIN32` 和 `_WIN64` 。 20 | 21 |   顺便,通常 64 位主要指 x86_64(微软称为 AMD64 ,这个兼容 x86 的基础架构一开始的确是AMD先搞出来的,后来才有 Intel EM64T )。 22 | 23 |   64 位 Itanium 也有 `_WIN64` ,不过一般见不到且跟MinGW没什么关系且现在都不正式支持了,不管了…… 24 | 25 |   对于 MinGW 来说,这里也有类似的坑:预定义宏得先优先检查 64 位的。实际情况更加复杂,另说。 26 | 27 | # MinGW 和 MinGW-W64 有什么区别? 28 | 29 |   这是个关键问题,但是……是个很长的故事。没有铺垫不好回答。 30 | 31 |   首先,MinGW 是 GNU 工具(包括编译器 GCC 和 GNU binutils 和调试器 GDB 等)在 Win32 上的一个移植,是从 Cygwin 里 fork 出来的。当初只考虑 32 位。和 Cygwin 相比,不强调 POSIX 兼容性而相对强调性能和减小依赖。 32 | 33 |   具体来说 MinGW 除了以上工具外,还提供了一个适配于 Win32 的运行时环境。其中 C 标准库实现的二进制文件直接用微软随 Windows 分发的 MSVCRT 。MinGW 自己的运行时库依赖于MSVCRT和其它系统库。 34 | 35 |   而 MinGW GCC 依赖于 MinGW 运行时以及 libgcc 和其它系统库。编译出来的程序一般也要依赖这些库,所以才会写死在默认 specs 里(可以用 `gcc -dumpspecs` 查看)免得用户随便编译链接个程序还得手动指定一大堆 `-l` 选项。 36 | 37 |   用三元组表示目标平台,当年的 MinGW 是指 i386-pc-mingw32 。这里 i386 也可以是 i486 等等……总之是 32 位 x86 指令集架构的名称;中间的 pc 可选,表示厂商名; mingw32 表示系统名。特别注意,事实上成为标准的“专有名词” mingw32 里的 32 是固定的;另外,所有这些大小写一般也是固定的。GCC 等的源码配置里面也有硬编码进去。 38 | 39 |   然后,因为只支持 32 位,有人觉得不够用。这里的一个主要人物,就是现在 MinGW-W64 的主要维护者 Kai Tietz 。[因为工作需要重新实现提供 x64 支持的 MinGW](https://sourceforge.net/p/mingw/mailman/message/23108552/) 后提交到上游但[被拒绝](https://sourceforge.net/p/mingw/mailman/message/23100595/),于是 fork 为单独的项目,这就是 MinGW-W64 的由来。 40 | 41 |   可见,MinGW-W64 和原版 MinGW 有所渊源,但是独立的两个项目。 42 | 43 |   W64 虽然用意是Win64,但是也算是个专有名词,在三元组里占据厂商名,例如常见的:i686-w64-mingw32。(在GCC源码的配置文件中,\*-w64-mingw32 和 \*-pc-mingw32 是分别对待的。) 44 | 45 |   MinGW-W64 是同时支持 32 位和 64 位的,甚至还支持 32 位和 64 位的交叉编译(启用 multilib 支持的 MinGW 发行版例如 mingw-builds 可以用 `-m32` 或 `-m64` 指定)。 46 | 47 |   显然,W64 和支持的架构无关。上面 i686 就不是 64 位的平台(而且可以看出这里的 32 也和架构没关系)。支持 64 位的对应三元组是 x86_64-w64-mingw32 。(另外 w32 是 GNU 惯用的对 Win32 的略称,也沿用到包括 MinGW 在内的一些项目中——如 [w32api](https://sourceforge.net/projects/mingw/files/MinGW/Base/w32api/) ,可能造成一些额外的混乱。) 48 | 49 |   ……容易让人头疼的是,这两个项目现在都没死,偏偏还很容易因为这些字面上的原因搞错。为了下文描述方便,原版 MinGW 称为 [MinGW.org](http://mingw.org/) 。 50 | 51 |   这里有一点非常重要:只有 MinGW-W64 是 GCC 官方支持的(尽管 [mingw32 平台是二等公民](https://gcc.gnu.org/gcc-4.9/criteria.html))。在很长一段时间内 Kai Tietz 拥有 GCC 官方 repo 的提交权限(虽然当前[已不再活跃而被移除](https://github.com/gcc-mirror/gcc/commit/e7d04cdd5ec5527790a278fb2a8bf8142d552f61#diff-bb5907b965b501e1856ccd79d2c4f642L145))。 52 | 53 |   所以,使用 MinGW-W64 的 GCC 一般比 MinGW.org 有更新更全面的支持,所以现在一般推荐 MinGW-W64 发行版。 54 | 55 |   说到这里……[维护 mingw.org 的 Keith Marshall 还和 Kai Tietz 等GCC官方人员在 bugzilla 上对噗过](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=52015) 。其中 Keith Marshall 对 MinGW-W64 使用 MinGW 一名造成混淆表示愤慨。嘛,这倒也是事实。 56 | 57 |   当然,也不是说 MinGW.org 就一无是处了。\*-w64-mingw32 原则上向后兼容 \*-pc-mingw32 ,不过也有一些接口上的差别。BSD 流的 `DT_*` 在 MinGW.org 上能用,在 MinGW-W64 的就没有。(虽然 `DT_*` 也不怎么推荐用就是了……) 58 | 59 | # 什么是 MinGW 发行版(distribution) ? 60 | 61 |   这个说法习惯上可以说是从 Linux 等软件中借用过来的。 62 | 63 |   类似 Linux 内核,不管 MinGW.org 还是 MinGW-W64 ,本身都是相对集中于特定软件包( MinGW 运行时库)开发的项目,并不着力于提供整个开箱即用的环境。 64 | 65 |   因此除了官方的一些编译版本外,有很多人基于 MinGW 运行时上进行定制封装供用户下载整个环境,有的还提供包管理服务等。这就是发行版。一般提供直接解压加上 `PATH` 就能用的环境和/或安装包。 66 | 67 |   早期比较著名的有 TDM-GCC 、rubenvb 等。以前用的 MinGW.org ,不过现在主要转到 MinGW-W64 上来了。 68 | 69 |   比较新的发行版,一开始就着眼于 MinGW-W64 。最著名的发行版之一应该是 mingw-builds ,基本上近年来( GCC4.7 以后) Windows 上能用支持最新版本最快的,支持交叉编译。 70 | 71 |   mingw-builds 一开始在 sf.net 上有[自己的项目](https://sourceforge.net/projects/mingwbuilds/),不过后来表示要求加入 [MinGW-W64项目](https://sourceforge.net/projects/mingw-w64) 作为 official builds ,所以停更了,更新都在 MinGW-W64 里面,不过残念的是好像到现在 MinGW-W64 看来都不提供唯一的官方发行版,所以还是叫做 personal builds 。 72 | 73 |   另外提一下还有微软 VC++ 标准库( Dinkumware 生产)维护者之一 [Mr.STL(Stephan T. Lavavej)](https://nuwen.net/stl.html) 个人的[发行版](https://nuwen.net/mingw.html) ,很早就在默认 specs 里加了 `-std=c++11` ,而GCC 5 改用 `-std=c++14` 。([官方 GCC 6 会用 `-std=gnu++14` 。](https://gcc.gnu.org/gcc-6/changes.html)) 74 | 75 |   还有 MSYS2 项目的 MinGW 发行版(这里可能有新的混乱,下文再说),也是 mingw-builds 一伙人搞的,4.9.1 比 mingw-builds 更新还快几个小时。 76 | 77 |   详细的发行版列表可以参照下文。 78 | 79 |   最后,不嫌闲着蛋疼也可以自己编译。不过迫不得已外最好别这样做( GCC 的编译过程和 hacking 实在无力吐槽)。重复一遍,非常不推荐。 80 | 81 | # 有哪些发行版可以选择? 82 | 83 | * 本机环境(直接在 Windows 下运行) 84 | * [MinGW-w64](https://sourceforge.net/projects/mingw-w64) 提供若干工具链的下载 85 | * [mingw-builds](https://sourceforge.net/projects/mingwbuilds/) 旧站点 86 | * [MSYS2](https://sourceforge.net/projects/msys2)(使用包管理器 `pacman` 安装 `mingw-w64-i686-toolchain` 或 `mingw-w64-x86_64-toolchain`) 87 | * [TDM-GCC](https://tdm-gcc.tdragon.net/) 88 | * [nuwen.net MinGW distro](https://nuwen.net/mingw.html) 仅提供 Win32 线程模型 89 | * [GCC-MCF](https://lhmouse.com/gcc-mcf/) 仅提供 MCF 实现 90 | * 交叉构建环境 91 | * [Arch Linux MinGW-w64 GCC](https://www.archlinux.org/packages/community/x86_64/mingw-w64-gcc/) 92 | * [MSYS2](https://sourceforge.net/projects/msys2)(使用包管理器 `pacman` 安装 `mingw-w64-cross-toolchain`) 93 | 94 | # MinGW 发行版支持什么本机语言编译器? 95 | 96 |   对于 C/C++ ,主要是 GCC 。GCC 也提供 FORTRAN 和 Ada 等语言的编译器。 97 | 98 |   除此之外,某些发行版(如 MSYS2 的 MinGW 环境)也带有兼容的 LLVM/Clang 工具链,但可用性差强人意;其中标准库实现仍然依赖 GCC 的 libstdc++ ,而不是 libc++ 。但一些可能提供的 Clang 附带的工具(如 `clang-format` )可用性基本不受限制。 99 | 100 | # 上面为什么要强调更新呢? 101 | 102 |   如果不想使用新的特性生成更高质量的代码,其实也没必要盯着上面这么多版本混乱的 MinGW 了。即便要兼容性,也可以用古董嘛(逃…… 103 | 104 |   对于 C++ 前端来说 MinGW 尤为重要,现阶段根本没有能顶替的。作为系统默认 ABI 新锐代表的 MSVC2015 Update n ——前端还是残的……各种 bug 。 105 | 106 |   GCC 也有各种傻缺 bug ,不过至少在前端来说,程度上绝逼打不过 `cl`( Microsoft C&C++ Optimizing Compiler )。 107 | 108 |   VC++ 调试支持当然好得多,但是编译器一坑爹集成调试再好也没用了。 109 | 110 |   嘛,Clang++ ?除非是 G++ 腻了换换口味还是得接着用 libstdc++ ,libc++ 什么时候能在 Windows 上跑顺再说——即便这样 MinGW 兼容的还是得依赖 MinGW 的 libgcc 。至于和 VC++ 兼容的 `clang-cl` ,看起来还在折腾微软的坑爹 ABI 黑箱(这没像大部分平台上 GCC 用的 [Itanium ABI](https://itanium-cxx-abi.github.io/cxx-abi/abi.html) 公开文档),一年半载别指望了。 111 | 112 | # 什么是异常模型和线程模型,用哪种比较好? 113 | 114 |   这两个都是对于 C++ 实现( G++ 、libgcc 、libsup++ 等等)而言的。 115 | 116 |   首先,异常模型。C++ 标准没规定异常怎么实现。MinGW GCC 使用的 Itanium ABI 也没规定必须怎么实现(但规定了一些公共接口),这部分由实现自行考虑。 117 | 118 |   GCC 一般提供了 SjLj( C 的 `setjmp`/`longjmp` )实现的 stub 。对于 x86 ,允许使用 Dwarf2 调试信息的实现。两者的区别在于 sjlj 比较通用,但是即便不抛出捕获异常而只是使用异常中立的风格隐式传递异常,也有运行时开销。而 Dwarf2 兼容性(考虑多层 C++ 和 C 的 DLL 互相调用来看)相对较差,但没有这种开销。 119 | 120 |   两者 ABI 并不兼容(知道 C++ 坑爹了吧,不仅不同实现不兼容,同一个编译器同一个平台自己都能跟自己不兼容……)——前者依赖类似 `libgcc_s_sjlj.dll` 这样的 DLL ,后者则是类似 `libgcc_s_dw2.dll` 这样的。旧一点的可能也没有这种后缀差异。 121 | 122 |   另外,libstdc++ 作为 C++ 标准库实现显然依赖异常,但名字一样的 DLL 可能依赖的不是同一种。所以混用多个版本 MinGW GCC 且没把 `PATH` 清理干净的时候容易出现找不到符号定义导致链接失败。这还不是最坑的,有时候 `gdb` 载入不同位置的 DLL 在运行时挂掉,还不只是一个 `PATH` 的问题……这种情况下先拿 [Sysinternals](https://technet.microsoft.com/en-us/sysinternals/bb545021.aspx) 的 [Process Exporler](https://technet.microsoft.com/en-us/sysinternals/bb896653.aspx) 之类的工具看看进程加载的 DLL 是不是预期的再说。 123 | 124 |   为什么说要有这么坑爹不兼容的,像 VC++ 一样用一种不就好了……其实 Win32 x86 上最理想的应该是和 VC++ 一样[基于 SEH( Windows 结构化异常处理)的实现](https://en.wikipedia.org/wiki/Microsoft-specific_exception_handling_mechanisms),但是 [Borland 关于这个的专利](https://www.google.com/patents/US5628016)才没过期几天……所以你懂的。 125 | 126 |   x64 上没专利的麻烦,有 SjLj 和 SEH 的实现,一般还是 SEH 。 127 | 128 |   第二,线程模型。 129 | 130 |   主要有两个,Win32 和 POSIX ,[对标准库线程的支持不一样](https://stackoverflow.com/questions/13212342/whats-the-difference-between-thread-posixs-and-thread-win32-in-gcc-port-of-windo) 。 131 | 132 |   Windows 线程 API 和 [POSIX(pthread)](https://en.wikipedia.org/wiki/POSIX_Threads) 有很大不同,而 ISO C++ 的 `std::thread` 为代表的接口是很接近 pthread 的。 133 | 134 |   所以在 libstdc++ 上实现这些接口,首先依赖的是 pthread 在 Win32 上的移植 libwinpthread ,也就是使用 POSIX 线程模型。因此发布的时候需要带上 `libwinpthread-1.dll` 之类的dll。 135 | 136 |   至于 Win32 线程模型,[GCC mailing list 是有提过](https://gcc.gnu.org/ml/libstdc++/2012-05/msg00020.html),不过到现在还是没实现。也就是说 ISO C++ 的实现是残的,没法用。如果只打算用 Win32 多线程 API 倒是的可以用。 137 | 138 |   所以取决于具体需要。要兼容性好点的一般还是 POSIX 。 139 | 140 |   较新的基于 [mcfgthread](https://github.com/lhmouse/mcfgthread) 实现的 MCF 线程模型可以替代 POSIX 线程模型,在 Windows 7 和更新的系统有较好的性能。 141 | 142 |   自 GCC 13 起,[mcfgthread 作为 `mcf` 现成模型已被合并到上游](https://gcc.gnu.org/git/?p=gcc.git;a=commit;h=f036d759ecee538555fa8c6b11963e4033732463)(虽然[配置文档](https://gcc.gnu.org/install/configure.html)没有同步更新)。 143 | 144 |   最后,还有单线程的 single 模型…… Windows 上应该没啥必要用。 145 | 146 | # 什么是 MSYS ,和 MinGW 有什么区别? 147 | 148 |   MSYS(minimal system) 原本是 MinGW.org 项目的一个组件,旨在 Windows 上提供一套类 UNIX shell 为基础的“系统”。它本身不提供编译器或者大小写敏感的文件系统支持(其实 [NTFS 倒是支持这里的“ POSIX 语义”](https://superuser.com/questions/364057),但基本没看见有谁用……)。 149 | 150 |   和作为原生 Win32 程序的 MinGW 不同,MSYS 环境下编译的本机程序依赖于额外的特定的 MSYS 运行时,更接近 Cygwin(强调 POSIX 兼容性),会有性能损失(但一般意义上比 Cygwin 轻量)。对应的[三元组](https://sourceforge.net/p/mingw-w64/wiki2/TypeTriplets/)是 \*-pc-msys(通常其中的 pc 可以省略即缩写为 \*-msys )。MSYS 提供了一个 sysroot 环境(下面有 `/bin` 和 `/etc` 等),因此移植 POSIX 环境的程序一般更方便。 151 | 152 |   所以常规的实践是,如果只是开发 Windows 程序,能用 MinGW 就不要用 MSYS 原生的编译器来构建。当然,使用 MSYS 上的 `sh` 等工具还是没问题,跟 GNU 工具配套怎么说比 `cmd` 好用。(虽然也有不少琐碎的兼容性问题。) 153 | 154 | # 什么是 MSYS2 ,MSYS2 上的 MinGW 发行版是怎么回事? 155 | 156 |   字面意思,MSYS 2.0 。比起 1.0 来说更加像 Cygwin(例如 `/etc/fstab` 配置)。项目[在 sf.net 上托管](https://sourceforge.net/projects/msys2/)。 157 | 158 |   其中的一个特色是基础系统附带 [ArchLinux](https://www.archlinux.org/) 移植的包管理器pacman,可以同时独立部署 `/mingw32` (i686-w64-mingw32) 和 `/mingw64`(x86_64-w64-mingw32) 下的开发和运行环境。注意和 mingw64 并列时 mingw32 自然指的不只是三元组的最后一项了。 159 | 160 |   下载依赖相当方便(就是没有靠谱的镜像时网速可能非常拙计)。具体使用参考 [Arch Linux Wiki](https://wiki.archlinux.org/index.php/Pacman)。对应的交叉编译环境在 Arch Linux 上也有[官方的包](https://www.archlinux.org/groups/x86_64/mingw-w64/)支持。 161 | 162 |   虽然 MSYS2 提供的 MinGW 上主要的编译器不直接支持交叉编译,不过可以同时装不同的目标平台的编译器(现在也有交叉编译器的包,没测试)所以不是什么问题,一定程度上比 mingw-builds 的 `-m32` 和 `-m64` 来说更加稳定靠谱。(现在也另外提供支持交叉编译的包。) 163 | 164 |   只提供 Dwarf2 异常模型和 POSIX 线程模型对于成套系统也不是什么大问题。包虽然比不上 ArchLinux 那么丰富不过常用的很多都有,免去自己编译的麻烦。打算长期使用 MinGW 和相关工具的,推荐使用。 165 | 166 |   虽然滚挂了也没多大事,不过版本是个问题。如果需要特定版本的 GCC 就不适用(比如规避GCC 4.9的坑爹bug……),除非有耐心自己找到 `.xz` 手动安装。 167 | 168 |   相关配置(包括 `pacman` 的一些中国大陆镜像)可以看[这里](https://bitbucket.org/FrankHB/yslib/wiki/Prerequisitions.zh-CN.md]) 。 169 | 170 |   除了 GCC ,较新版本的 MSYS2 也提供了 Clang 的支持和使用 ucrt 代替 msvcrt 作为 C 运行时库的工具链。在 Windows 11 ARM64 上,MinGW Clang 可以支持 [64 位 ARM 架构](https://www.msys2.org/wiki/arm64/)。 171 | 172 |   综上,MSYS2 [现已支持以下环境](https://www.msys2.org/docs/environments/)组合: 173 | 174 | * MSYS 175 | * UCRT64 176 | * CLANG64 177 | * CLANGARM64 178 | * CLANG32 179 | * MINGW64 180 | * MINGW32 181 | 182 |   除了本机的 MSYS 环境,其它都是 MinGW 环境。这些环境可以在一个 MSYS2 安装中共存。而最早期的 MinGW 发行版只相当于支持 MINGW32 。 183 | 184 |   使用 MSYS 和 MinGW 共存的环境时混用不同环境的工具,因此需要注意[文件系统路径](https://www.msys2.org/docs/filesystem-paths/)等问题。 185 | 186 | # 部署程序需要提供哪些文件? 187 | 188 |   Windows 默认安装自然不带 MinGW 运行时环境,所以除了编译出来的程序和可能附带的数据,一些dll是要准备好的——除非有耐心折腾全部静态链接。 189 | 190 |   不同版本不同语言不同编译器编译出来的东西都不太一样。最简单暴力也可靠(?)的方法就是复制可执行程序到没装环境的白板测试机上看少了哪些东西(不过未必一目了然)。 191 | 192 |   简单可靠的方式是用 [Dependency Walker](https://www.dependencywalker.com/) 等工具查看依赖。 193 | 194 |   对于 C++ 程序,除非不用 POSIX thread 可以省掉 libwinpthread ,一般至少得确保上面异常模型和线程模型讨论中提到的三个 DLL(注意就算不显式使用标准库,编译器生成的代码也可能用到——典型的如默认 `::operator new` ,所以得带上 libstdc++ )。 195 | 196 |   使用 MCF 线程模型需要部署对应的 mcfgthread 动态库(如 `mcfgthread-9.dll` )。 197 | 198 | # 还有什么其它问题? 199 | 200 | ## LTO 201 | 202 |   GCC 4.9 的 LTO(链接时优化)默认使用新的目标文件格式,生成的文件不包含冗余的二进制代码。 203 | 204 |   但是 LTO 有特定的 phase([内部会调用 `make` 多编译几个 pass](https://gcc.gnu.org/onlinedocs/gccint/LTO-Overview.html#LTO-Overview) ),传统的静态链接器(`ar`) 不知道这里的约定,所以原来好好的东西,升级 4.9 以后开了 `-flto` 就可能找不到符号链接失败。 205 | 206 |   许多 MinGW 发行版仍然没实现 `gcc-ar`(运行会提示没支持 linker plugin )。兼容旧版本的行为还得加上 `-ffat-lto-objects` 编译选项。 207 | 208 |   较新版本可能解决了这个问题。 209 | 210 |   此外,内联 vtable 开启 LTO 导致链接失败。此时应保证非内联的虚函数,使 vtable 在特定翻译单元内生成;这样也有利于编译性能。 211 | 212 | ## PIC 和 PIE 213 | 214 |   PIC( position-independent code ,位置无关代码)和 PIE(position-independent executable ,位置无关可执行文件)引起目标代码生成中关于寻址方式的变化,允许更简单地实现重定位而共享加载的动态库二进制代码,以及使加载的代码不适用绝对地址而实现 ASLR(address space layout randomization,地址空间随机化)提升安全性。这在许多平台上生成动态库时需要;但 Windows 上,显式的 `-fPIC`/`-fPIE` 或 `-fpic`/`-fpie` 选项是多余的,一般应当避免使用: 215 | 216 | * 因为 ABI 要求,32 位 Windows 不使用 PIC/PIE 。 217 | * 因为 ABI 要求,x86_64 位 Windows 总是相当于使用 PIC 。 218 | * 较旧版本的 GCC 可能生成提示这些选项多余的警告,但[提示的原因具有误导性,而被移除](https://gcc.gnu.org/legacy-ml/gcc-patches/2015-08/msg00836.html)。 219 | * 其它编译器如 Clang++ 可能明确不在 MinGW 支持这些选项。 220 | 221 |   原则上 PIC/PIE 相较不适用 PIC/PIE 的绝对寻址的代码会引入一些开销。为了使这项开销尽可能小,体系结构可支持高效的相对当前指令位置寻址的访存方式。然而,尽管[这是 70 年代的 PDP-11 就支持的特性,IA-32 长期以来不支持](https://stackoverflow.com/questions/31332299/31332516),直至 x86_64 支持 RIP 相对寻址。这是 Windows 的 ABI 的差异的主要技术理由。 222 | 223 | # 附录:发行版选择问题 224 | 225 |   一直以来,Win32 上存在支持 GCC(以及可能有 LLVM/Clang)的发行版,包括且不限于: 226 | 227 | * MSYS2 228 | * MCF 229 | * MinGW-builds 230 | * rubenvb 231 | * nuwen.net 232 | * winlibs.com 233 | * 可能还有其它的…… 234 | 235 |   大多还在长期地持续更新。 236 | 237 |   尽管上游(至少 GCC+Binutils )原则上在同一个目标(三元组)之间保持相当强的二进制兼容性,这些发行版因为使用的线程/异常模型组合等略有不同,甚至(可能)采用的构建编译器和运行时补丁的不同,之间并不能有效地确保(构建输出的)二进制兼容性。(很显然,没人能有效地测试**所有**这些版本。) 238 | 239 |   因此只要涉及二进制输出程序地分发,选择发行版的策略就这不仅仅是关于治好开发者的强迫症的问题,而会以更微妙的方式影响到下游的(最终)用户。 240 | 241 |   关于一般建议……嗯,太长不看,这里先说结论: 242 | 243 |   目前(截止 2022-04 ),如果不是有十分确定的理由,对一般用户(开发者),仍然建议默认*无脑(blindly)* 使用 MSYS2 自带的 MinGW 工具链发行版。 244 | 245 |   一些简要的理由大纲: 246 | 247 | * 首先现在一般默认 MinGW-w64 而不是 MinGW.org(这个上面章节有提过)。 248 | * MSYS2 的支持的构建目标(target) 比较全面。 249 | * 同时支持 32/64 位(i686/x86_64) 这个基本都做得到,略过。 250 | * GCC 可支持不同的 libc ,除了传统的 MSVCRT ,也支持 UCRT 。 251 | * 除了 GCC ,也支持 Clang 。 252 | * 使用 ARM64 架构的 Windows ,Clang 支持 64 位 ARM 架构(aarch64) 本机构建。 253 | * 较传统的发行版缺少这些全面的支持选项。 254 | * 同时有 `pacman` ,这个是核心竞争力。不用费心部署问题了。 255 | * 不过反过来,因为不放心依赖 MSYS2 中的非 MinGW 本机环境(而依赖了 MSYS2 dll )或者不太有替代品的包,想要避免过于依赖 MSYS2 ,可以选其它的环境测试。 256 | * 非 MSYS2 的性能可能偶然会更好,但无法排除偶然性——确定是 MSYS2 的发行构建机制上而不是别的什么原因(特别是上游工具链版本的问题)导致 MSYS2 发行版确实更慢。 257 | * 除了避免太依赖 MSYS2 ,用户没什么其它实际理由去特意去用发行版。 258 | * 因为基本上都是 MSYS2 的功能的子集。 259 | * 唯一一个例外是 MCF 提供了不同的线程模型,这不是一般开发者能自己动手提供替代扩展出来的功能。 260 | * 不过这现在只支持 GCC ,不支持 LLVM/Clang 。更显著地,这还不是 GCC 上游支持的线程模型。所以用了这个,基本也不要有跨发行版的兼容性了。 261 | * 现实来讲,MCF 的合理用途是较特定运行环境特定下的优化。不过因为不是上游支持,案例可能很少(如果不是找不到),现成的二进制包也少,所以有多大坑用户得自己评估(尽管理论上切换线程模型对绝大多数上层应用程序应该是透明的)。 262 | * 总的来讲,这可能比一般用户想像得更面向专家一点。 263 | * 上面的理由都是对用户(一般开发者)而言;如果就是自己想做一个新的发行版自建生态,那当然也能算个合理的理由。 264 | * 不过,除非一口气发展出比 MSYS2 功能更全面的发行版,这不可能解决对现有用户而言选择已经太多的问题。 265 | 266 |   以下详细分析个别关键决策问题。 267 | 268 | ## 目的性 269 | 270 |   虽然带有一些主观性,不过支持的目标可以从文档中看出来。 271 | 272 |   (什么,没文档?依赖这样的项目自求多福吧。) 273 | 274 |   一个例子是 [winlibs.com 说明了发行版的哲学](https://winlibs.com/philosophy.html)。不过,仔细阅读之后,还是不难发现问题: 275 | 276 | * 没说清楚跟 MSYS2 根本上有什么不同。 277 | * 相反,除了缺少 `pacman` ,没那么多包,几乎就和 MSYS2 的选择一样。 278 | * 虽然偶尔也考虑过其它选项,但看上去也没很确切地详细分析清楚了。 279 | * 例如,[有 C 用户建议 32 位使用 SjLj 异常模型](https://github.com/brechtsanders/winlibs_mingw/issues/20),作者并没有十分确切地解释清楚作出拒绝决策的理由。 280 | * 事实上,原提议很大程度是不必要的。SjLj 真正的技术优势在于 C 代码和可能存在 C++ 异常的代码的互操作性,反而不是纯 C 用户能明显感知的。要缩小二进制体积,原则上 `-fno-exceptions` 已经足够。 281 | * 就为什么 64 位使用 SEH 而 32 位没有跟进的问题,也没有明确试图解决和推进。 282 | 283 |   相比而言,winlibs.com 其实已经算是表现比较完善的发行版了(起码有名为哲学的文档)。大部分其它发行版的主张更模糊,用户很大程度上只能通过“提供了什么”来判断是不是适合自身的需求。那么为什么不默认直接选择提供的支持更完善,一般更容易多提供些什么的 MSYS2 发行版呢? 284 | 285 | ## 性能测试 286 | 287 |   评估工具链这样复杂软件的性能一般是困难和专业的问题。而就现状而言,并没有足够可信的来源表明这些发行版(即便只是最主要的几个)之间具有(除了上游版本之外的)决定性的普遍差异。 288 | 289 |   偶然可能会有一些用户试图简要地给出一些选型结论,例如 [Codeforces 的测试](https://codeforces.com/blog/entry/96040)(附带[数据表格](https://docs.google.com/spreadsheets/d/1nOu7wJ1bB-z0WlkkS1FdYMrEoA10_G6f7ccINbSq-jU/edit?usp=sharing))。不过,这明显有一些技术问题: 290 | 291 | * 比较的工具链版本不同。 292 | * 表格中的日期是 2021-10-14 ,只有 winlibs.com 的 GCC 是 11.2.0 的版本。事实上,[MSYS2 的 GCC 在之后几天](https://mirrors.tuna.tsinghua.edu.cn/msys2/mingw/mingw64/)发布了 11.2.0 的版本。这个比较似乎有点操之过急。 293 | * 即便需要立即比较,当时 MSYS2 也有 GCC 10.3.0 。考虑滚动更新相当容易,为什么不升级到当时的最新版本、和 11.2.0 更接近的版本比较? 294 | * 没有给出具体的测试用例的源代码和构建环境配置。 295 | *(从名称上看的)一些 I/O 和数据转换的测试的结果看上去相当可疑,无法排除主要原因来自上游(编译器和标准库的实现)的差异。 296 | * 例如涉及浮点数的测试,结果大致上分为两组,慢的都是上游版本低的。 297 | * 还有反过来的,像 `cout_int` ,除了显示上游影响的可能性,还和总的结论矛盾。 298 | * 因为没测试用例代码也没具体输出结果,很难确定数据的正确性(不排除一些实现对精度上的妥协)。 299 | 300 |   这样的测试能体现出的实用上的通用建议也许只有一个:尽量考虑选择较新版本的上游。除此之外,缺乏其它可靠的系统性对比评测时,不建议照搬类似的决策。 301 | 302 | -------------------------------------------------------------------------------- /zh-CN/move-vs-copy.md: -------------------------------------------------------------------------------- 1 | # 转移 vs 复制 2 | 3 | Created @ 2013-06-19, r1 rev 2013-06-19, markdown @ 2015-09-15. 4 | 5 |   ISO C++11 引入右值引用以及转移构造等机制作为转移语义的内建支持。对应原本的复制构造等机制抽象的复制语义,可以说和转移至少在语言层次上是并列的。 6 |   那么转移和复制有什么共同点和区别?它们的关系如何?以下讨论几个相关的问题。 7 | 8 | # 1.不论具体的语言支持,一般意义上的复制/转移语义抽象 9 | 10 |   复制和转移是使程序从一个执行状态迁移到另一个执行状态的操作。不过依赖的概念可能有些模糊。方便起见,这里约定“值”表示某个抽象的单一(可分辨)状态,假设“变量”是可变的值的容器。 11 | 12 |   对于最简单情况,值可以取空值(记作 `e` )和非空的非平凡值(记作 `v` )。 13 | 14 |   (不过,一般这样的抽象是不够的,因为复制和转移本质上表示的不仅是决定性(deterministic)操作,前后两个状态可以有未指定的值。因此可以引入一个表示变量的未指定的取值 `?` 。) 15 | 16 |   设程序 `P` 的两个执行状态 `E1` 和 `E2` ,每个执行状态有两个取值状态(“变量”) `V1` 和 `V2` ,分别可以取值 `v` 和 `e` ,也可能是 `?` 。 17 | 18 |   `E1` 为 `P` 的初始状态,满足 `E1 = {{V1, V2} | V1 = v}` 。以`→`表示映射,那么有: 19 | 20 | 复制操作 `C: E1 → E2` 满足 `E2 = {{V1, V2} | V1 = v ∧ V2 = v}` ; 21 | 22 | 转移操作 `M: E1 → E2` 满足 `E2 = {{V1, V2} | V2 = v}` 。 23 | 24 |   直观理解,一个变量的值在复制后出现在原来的变量和另一个其它变量中;一个变量的值在转移后出现在另一个其它变量中。 25 | 26 | # 2.结构化复制和转移 27 | 28 |   为了在语言中表达复制和转移,需要借助类型系统及约定的原始操作。 29 | 30 |   对于复制来说,除了少数“纯函数式”的语言,基本的操作是赋值(本身就是一种复制)。相反,可能由于复制可以用转移和赋“空值”复合,一般语言不直接提供转移操作。 31 | 32 |   实际上,赋值作为简单的复制一般是不够用的。一般来说,语言提供的抽象手段允许复用基本操作,对于构建复制和转移操作也不例外。和经典的函数调用类似,这里的有效的复用可以是结构化的:递归的且有层次性的。 33 | 34 |   C++ 提供了复制构造表达一种符合结构化程序思路来构建复制(初始化)语义:自定义类型的复制使用成员复制(以及构造函数体的其它副作用),直至基本类型的复制(同赋值)。自定义类型的复制构造反过来也成为了复制赋值的基础。 35 | 36 |   C++11 的转移构造也使用类似的机制。而对于基本类型来说,复制和转移在这里是一致的。 37 | 38 |   从上面的形式可以看出,复制实质蕴含转移:任何复制都是转移,复制是附加了更强约束的转移。 39 | 40 |   逻辑上转移的实现似乎可以用复制和删除取代,或者扩展转移机制以实现复制。看起来似乎提供复制和转移两套近似的机制有点浪费。那么: 41 | 42 | a. 复制能代替转移吗? 43 | 44 |   答案是可行的——事实上 C++11 前就是这样做的。但是 C++ 在这里有一个特性——复制构造允许复制成员以外的副作用——导致了致命的弱点:复制开销无法被轻易优化掉。为了克服这点,ISO C++ 甚至有专门的语义规则允许忽略复制的副作用。区分复制和转移允许更好地减少不必要的开销。在逻辑上也有更重要和普遍的漏洞。原理见下文。 45 | 46 | b. 转移能代替复制吗? 47 | 48 |   让转移成为更“基本”的复制,扩展转移以实现复制至少对于 C++ 来说也不符合实际。复制无法被转移直接取代,除非允许重载基本类型的操作——如果是这样,基本类型的操作又用什么表示呢?在 C++ 的框架内部行不通,而暴露底层实现(如汇编)给用户看来也不是干净的做法。 49 | 50 | # 3.控制流中断:异常安全 51 | 52 |   比较 `{{V1, V2} | V1 = v}` 、`{{V1, V2} | V1 = v ∧ V2 = v}` 和 `{{V1, V2} | V2 = v}` ,可见复制操作和转移操作在这里有一个根本的不同:复制操作需要多维持一个状态(“变量”)。 53 | 54 |   从体系结构上(注意,并不限于具体语言的实现)来说,若不限定状态在语言实现上的某一个 `phase of execution` (普遍地,如编译时),复制操作必然需要申请获取资源(典型地,如内存);除非预留(本质上来说和限定 `phase` 类似,所谓“静态”的优化),这种资源的分配总是可能失败。 55 | 56 |   一旦失败,依赖于被复制的状态的操作就不能进行,即错误必须被处理;否则逻辑上就不符合期望了——也就是错的。由于基本操作的普遍性,让控制流中断而不是让用户在调用端手动检查是比较简便的做法:在 C++ 中即抛出异常。(因为允许失败以及析构函数保证的决定性生存期终止语义,“变量”—— C++ 的对象可以很直接地作为资源的抽象,即 RAII 惯用法。) 57 | 58 |   上面提到过的复制和转移是结构化的,这意味着除了基本类型(复制和转移一样,没有异常)外在任何层次都得考虑这个问题。只要有一层复制初始化操作(复制或转移)可能出现异常,那么依赖于这个不保证无异常的操作的复制初始化都不能幸免。也就是说复制可能抛出异常这点在一般意义上是普遍的(注意C++03的实践已经证明了限制异常规范的做法的失败)。 59 | 60 |   而转移操作没有这个限制。理论上来说,任何转移操作总是可以被设计为保证成功的。所以转移操作往往用 `noexcept` 限定没有异常;标准库的泛型组件一般也要求被操作的对象类型可无异常抛出转移。 61 | 62 |   考虑到异常安全和无异常抛出保证实现的困难性,这个差异是如此重要和普遍,以至于即便无视复制初始化的性能问题(毕竟还可能让 RVO 等挽救一下),单独区分出转移也是相当有意义的。 63 | 64 | # 4.转移和交换 65 | 66 |   注意 `M: E1 → E2` 中,`E1` 和 `E2` 是对称的。这意味着两点: 67 | 68 | a. 转移本质上是一种(对称的)状态迁移; 69 | 70 | b. E1和E2作为执行状态可以置换。 71 | 72 |   对于 C++ 来说,a. 的意义在于如果转移可以用简单的操作实现。注意对象转移后仍然会被销毁。对于基本类型对象,因为没有有副作用的析构(non-trvial destructor) ,所以可以直接复制/赋值(这再次说明了基本类型复制和转移的一致性)。 73 | 74 |   不过对于整体状态来说还有个陷阱,导致程序不一定总是能得到期望行为。`{{V1, V2} | V2 = v}` 隐式地蕴含 “`V1 = ?`”,即被转移的“变量”的状态是无法保证可预期的。如果需要可预期(仍然能足够安全地使用被转移了的资源),那么简单复制就不一定行得通了。比如说一个预期保持所有权、通过 `delete` 释放的内建指针,如果复制代替转移而不清空,最后会导致多次 `delete` 。 75 |   很遗憾这里C++无法有效地提供安全检查。一种安全的惯用法是使用交换例程(如 `std::swap` )来实现,不过需要指定的类型可交换。这早就是一种异常安全代码的常见技巧——C++11也约定 swappable 要求无异常抛出。 76 | 77 |   b. 是 C++ 语言层次上难以利用的特性(和复制构造类似,转移操作也允许存在用户自定义的副作用,省了转移的优化也是受限的),在此不讨论(嘛,Prolog 什么的就算了……)。 78 | 79 | # 5.转移语义:显式实现 vs 语言特性 80 | 81 |   一个语言可以不直接支持转移需要的特性,而只是支持间接操作的“引用”类型,这样仍然可以实现转移。这时候典型的转移实现被称为“浅复制”(shallow copy) ,区分于完全值语义的(递归)“深复制”(deep copy) 。 82 | 83 |   对于语言设计来说,这可以使设计简化,而不必像 C++11 那样引入右值引用类型来折腾类型系统。不过,这样往往意味着用户需要完成更多的重复代码。至少对于 C++ 这样对象模型中显式声称“对象即存储”精神(继承于 C )来说的语言来说是这样。 84 | 85 |   从实际需求出发,理想情况下,语言应该能够分辨出转移和复制这两种不同的需求。像 C++11 在类型系统上做的手脚已经被证明可行的,那么除此之外,有没有比让用户显式实现更简单的方法呢?上面说过,至少在 C++ 中复制和转移无法互相取代,不过如果不限 C++ 嘛—— 86 | 87 | a. 转移取代复制。要是提供允许用户指定行为的基本类型操作的公开接口,那么转移就可能直接作为复制的基础了。不过,因为C++仅有的静态分派和多态(重载)依赖于类型系统,像C++要这么搞恐怕类型上的折腾还是省不掉……而实际上,分派也好模式匹配也好需要的签名要是类型以外的其它新特性,本质上来说也属于传统类型理论的范畴内。 88 | 89 | b. 复制取代转移。不使用显式类型,取消复制初始化的副作用,让语言实现预测复制到底需要怎么实现。这是常见函数式语言的做法。这种复制相对很容易优化掉,代价是实体(“变量”)无法隐式地作为具有状态的资源抽象。 90 | 91 |   至于 C 、Java 什么的……算了,老实人肉实现吧。 92 | 93 |   所以 C++ 其实也算个典型,虽然看上去糟烂混乱不堪大用,不过小心点用也还算差强人意了。 94 | 95 | # 6.结论 96 | 97 |   坑掉算了。 98 | 99 | -------------------------------------------------------------------------------- /zh-CN/philosophy-of-make-decision.md: -------------------------------------------------------------------------------- 1 | # 决策的哲学 2 | 3 |   许多解决方案设计的问题上原则是通用的。本文档提纲挈领,汇总一些相关话题、通用的准则以及例子备考。 4 | 5 | ## 决定性问题 6 | 7 |   一类基本的决策问题是[决定性问题](https://zh.wikipedia.org/zh-cn/%E6%B1%BA%E5%AE%9A%E6%80%A7%E5%95%8F%E9%A1%8C)。 8 | 9 |   作为需要解决的问题,首先一般需要确定是否是可解的。而判定是否可解是一个决定性问题。 10 | 11 |   决定性问题是可解的算法问题中是相对容易的一类。这里的难易程度指容易分类或者规约到其它一类问题以采用算法步骤解决,不表示为了解决实际是否容易投入资源实现算法。 12 | 13 |   一般的算法问题有时可以规约为一系列等效的决定性问题,即分解问题为判定两种选项之一的**原子问题**。但即便是最简单的问题形式,也不见得容易解决,因为原子问题的原子性是关于已知计算模型上的算法过程的,而在考虑现实可解性(例如是否可行)上可能蕴含非算法问题,其中包含主观的**价值判断**,而不仅是能通过算法处理的**事实判断**。 14 | 15 |   尽管对理性人,基于确定效用模型的价值判断仍然是算法过程,但现实中这些效用的输入以及对应的偏好往往跟随环境改变。因此,需要锚定一般的原则以减少影响。 16 | 17 |   为了使解尽可能有效和容易重现,应当: 18 | 19 | * **避免路径依赖**:选择的策略依赖代价和意义,而不依赖路径。 20 | * 这包括依赖**当前**的代价和意义,以及**迟早**或**最终**遇到的未来的代价和意义,如果这些是存在(足够收敛能确切感知)的。 21 | * 例如,不应当认为因为你在幼儿园学习个别初等算术时就接触和熟悉了中缀语法,而认为中缀语法比你之后了解的其它记法普遍更自然。 22 | * 尽管幼儿园学习初等算术比系统掌握更一般地理论看似更容易(因为知识的总量更少更简单),但只要学习不终止于幼儿园,**迟早**要面对更一般的方法。 23 | 24 | ## 非对称性 25 | 26 |   考虑在决策问题中,选项 A 和 B 中选取一种的决定性问题的原子问题。 27 | 28 |   忽略偏好变化,实时的简单判定算法很简单:取对选项 A 和 B 效用偏好的序,若相同则随机。 29 | 30 |   若需要考虑偏好,则需要修正。一般的做法是:假想在可预见的未来(决策的影响会生效时),A 和 B 的长期效用的得失。 31 | 32 |   一部分很重要的效用来自(相对)客观的、不容易受到偏好变化影响的外部事实。因此,对事实的理解可以影响决策。一般地,这种影响对 A 和 B 不均等,而可对决策作出非平凡(non-trivial) 的影响。这种影响被认为是“有意义”的而不可被随意忽略。 33 | 34 |   更一般地,至少在统计上,承认以下前提是自然的: 35 | 36 | * 承认“有足够意义”的客观世界普遍是*不对称的(asymmetric)* ,并由此使很多人为设计具有独立的不可替代的意义而产生价值。 37 | * 例如,*封装(encapsulation)* 的一般意义就是来自于一种接口可以和不确定数量的实现方式对应。 38 | * 接口及其实现的地位不能调换,可以调换的 1:1 设计是平凡的(trivial) ,欠缺封装的意义。 39 | 40 | ## 领域设计参照 41 | 42 |   关于现成的一个系统的例子参见 [NPL 领域设计原则](https://frankhb.github.io/YSLib-book/Features/NPL.zh-CN.html#%E9%A2%86%E5%9F%9F%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99) 。 43 | 44 |   当然,直接看大概容易会有这样的感觉: 45 | 46 | ![如何画马](https://imgsa.baidu.com/forum/pic/item/26a8b754564e9258f626578f9d82d158cdbf4ec8.jpg)。 47 | 48 | (声明:图像版权不明,此处为合理使用。) 49 | 50 |   这没办法,因为本文档本来就是总结高频出现的观点以备复用,过程作为技术细节,需要被使用的需求并不常见,我就懒得写了。 51 | 52 |   不过有个愿景:作为比较简单的任务,这些步骤应是能被未来的 AGI 填充的。基于炼丹的那些就算了。 53 | 54 | # 语言设计上的应用 55 | 56 |   在一些品味(flavor) 的问题上,难免时常难以找到足够客观的判断依据。但明知道不够客观,分开阐述一些逻辑前提,还是更有建设性的——至少比所谓“优雅”“学院派”的 buzzword 清晰地多,因为可以明确*目的*。而因为涉及主观价值判断,这里自然上升为哲学(伦理)问题。 57 | 58 |   这里隐含的出发点即非对称性[非对称性](#非对称性)和应当[避免路径依赖](#决定性问题)。 59 | 60 | ## 直接应用 61 | 62 |   对语言设计品味的评价的一些结论,可以由上面的前提经过少数几步推理过程得到。 63 | 64 | * 例如,在默认蕴含 GC 的语言设计中让用户程序去除 GC 远比不要求 GC 的语言中通过用户程序引入 GC 的工程代价大得多。考虑到没有 GC 能够实现有 GC 时不容易实现的程序性质(如确定性资源管理、低延迟、低内存开销)。作为进一步的推论,一般的通用目的语言应当避免蕴含 GC ,以能适应最大化的情况。 65 | * 又如,计算和逻辑不对等——存在偏可计算函数,不存在偏证明。 66 | * 计算系统和逻辑系统的目的就不是对称的。 67 | * 并且,前者可以作为后者的基础,反过来不行。 68 | * 纯 FP 党根据 Curry-Howard 同构到处瞎找对偶(duality) 很大程度是徒劳的,因为不能认为一种结构具有目的,就一定能起到相同的作用。 69 | * 加入 Lambek 后的范畴论风格两两组队,具有同样的问题。范畴论语言很难在非结构主义数学家的圈外推广,一个方面就是理解上以及使用上的代价(比如罗嗦)跟传统方法都不是对称的。 70 | 71 | ## 递归和循环 72 | 73 |   作为经常被误解到值得单独一提的例子,递归和循环比较起来就不是对等的。 74 | 75 |   从可编程设施的接口的目的来看,迭代(循环)和尾递归具有同等难以理解的抽象程度和普遍性,初学者真要强行理解那只有具体实现了(例如具体的 `while`),有的派生(如各种 `for` )语法上还更加麻烦。因为不可能就循环这个概念上强加个全称量词用户确保举一反三,实际上入门的用户根本没法教。真直接抽象的也就是“我要重复几次”之类的声明式描述,其实现对初学者而言还是黑魔法(而且基本也没办法准确实现,比如重复几次是或者不是并行的,副作用每次不同的又不确定相对顺序的,这些实现之间能共用么)。要准确领会抽象的迭代一般是很久之后的事了。反过来,递归在高级语言中基本上只有一种语法,基本不需要关键字。而且直接不需要什么多套娃几层抽象,直接教就是——这还不说尾递归就是加了个特殊条件。 76 | 77 |   有人认为递归比循环麻烦,以至于扣了“学院派”帽子就跑。支持这种观点的依据纯属无中生有。 78 | 79 |   如果是像 JavaScript 这样广泛使用的现有工业语言,为了维护现有代码的兼容性(包括业已鱼龙混杂的现有非专业用户的智商),外加当初的目标本来就不高(不要求那么通用),不去纠正现有设计上显然的技术错误也还情有可原。但抛开具体现有语言不管,这种调调就是扯蛋。 80 | 81 |   理解不了递归比循环自然,对一个实用的通用语言(而不是备胎或者大作业)的设计者来说,简直就像是理解不了 `2 > 1` 。 82 | 83 |   这里顶层功能点复杂度的计数是一目了然的事情:`card({递归}) = 1` 且 `card({递归, 循环}) = 2`;而只有循环没有递归的语言几乎就是低级语言(于是直接不配当通用目的语言),因为: 84 | 85 | * 要重入的函数应用时还得 hack 掉活动记录(activation record) 之类本来可以不用出现在简单配置中的细节甚至干脆让用户自己实现一整坨。 86 | * 如 ISO C 半点没提 the stack 。 87 | * 搞笑的是因为一般体系结构上的 native stack 不靠谱所以其实已经不少靠谱的语言都自己造活动记录了。 88 | * 而重入函数调用是现实不可避免的。 89 | * 光是过程式范式就到处都在用了。 90 | * 并且*在工程上*远远比普及手动造栈这种无聊易错增加实现和验证成本的人肉编译器工作好使得多。 91 | 92 |   注意,在所谓的循环控制结构不配取代递归调用的前提下,这里的 `2 > 1` 就是不对称的实例。所谓的循环用递归实现,而不是反过来,这是另一种自然的不对称。这种实现策略中,循环只是高一层的抽象的惯用法,递归调用是所谓的循环的实现。你说只学循环不学递归行不行?对外行或者 DSL 用户或许有些绕弯路的可行性(因为危害不会即时爆发,对非专业人士可能不影响实际的干活;这也是一个普遍能绥靖的原因),但对专业用户不行——他们迟早会遇上得自己发明正经的递归特性才能自然支持的实际需求——这种需求来源于算法设计(比如树的遍历),根本上来自甲方,不是你个实现人员想干掉就都能干掉的。另一方面,在这个角度上,把递归算法过程分为空间复杂度意义上能等价实现为循环的(尾递归)和不能实现为循环的,又是递归这种实现细节的*抽象泄漏(abstraction leak)* 。反正递归(调用)这种东西在高级语言横竖都要学而且更有力,为什么要逃避?更不说一般的递归根本不只是调用这种隐含的控制操作那么简单,而是普遍得多,根本没所谓的循环能对应的东西——比如文法产生式的自然复用的表达方式,比如 μ 递归函数这样的替代可计算原语。凭什么面对能正经认识这些堪称计算的基础常识的*那么简单*的东西(就是纯粹的认知演绎,又没要求让你保证会构造 Church 数的前驱操作或者做一坨离谱的证明题这种可能需要技巧性天赋的东西;而且几乎肯定比可能某些导论课就引入的那种不大严格的算法分析还简单——毕竟就算没严格形式语言,也有的是有递归的伪代码让你折腾的)的*专业人士*,还要造谣粉饰认知难度会有问题?甚至非得另外夹带私活塞进什么鸟《编译原理》这种设计上基本全是基于蓝星猴子对“计算”认识的历史的支线和野史的弱鸡课里,打包到不起眼的位置上选修?这些操作是在侮辱谁?非得没事让用户走弯路自己发明一遍历史才叫卷是吧?嫌弃**人矿太多了**是吧? 93 | 94 |   真学院派起来,[用迭代建模递归的正经做法也有,需要超纲得多的条件](https://okmij.org/ftp/continuations/undelimited.html#iter-recur),以至于相当非主流。可见这种对不对称性的无视根本不分什么学不学院派——无非是学院派挖得更深所以无能的东西没那么能力成为主流。像什么[对象演算(相对于 λ 演算)](https://cs.stackexchange.com/questions/18963)也是,基于类之类的记录(record) 本来就是闭包(closure) 和环境(environment) 缝合起来的东西,非得下克上充当 primitives ,当然别扭——说白了除了没事搞煋闻氵 paper(当然,要能推广了证明这是里的路线多欠揍而不可行,在学术上不是坏事),就是脑子不好使,非得往具体的无能欠揍弱鸡设计去对齐,自身目无原始需求,连对执行的上下文(context) 合理分类都做不到(顺带,另一个重要的上下文是续延(continuation) )。 95 | 96 |   为什么有些看似应当足够内行的用户会连这些基本的现象都没发现呢?我猜: 97 | 98 | * 首先是因为没天赋(正常)有能耐直接无中生有这些说多不多、说少也不少的 PL 常识。 99 | * 特别考虑到,翻车走错路的设计在历史上比比皆是,包括 C/C++ 那种随便炸栈 UB 却还有连让人拿来当入门语言的搞得用户有**不敢推翻的思想包袱**乃至**教条主义和权威主义**,都得不良倾向过滤掉,返工的工作量一下子就上来了,根本不是再多学一个两个流行语言甚至范式能解决的。 100 | * 更别说有余裕用用之后学到的或者自己凭空领悟到的普遍完善的理论体系重建新的框架,填充上这些次品知识中尚且能用的部分了。 101 | * 同时,**教育质量普遍太烂**,入门的语言就是大杂烩一箩筐姿势不对,根本没基础静心独立分解特性分析清楚,天赋普通又没本事重新把理论从零发明一遍,也没能力搜得到非主流文献做对比。 102 | * 顺便,以上几点也是我强调对现在有些计科[入门姿势](https://github.com/FrankHB/pl-docs/blob/master/zh-CN/introduction-to-learning-computer-languages.md)乃至[整个理论体系](https://www.bilibili.com/read/cv26671044/)普遍不满的原因。 103 | * 外加个俗人常见的问题:缺乏自知之明。 104 | * 对历史和现实的力量都几乎一无所知。 105 | * 对自己的能力也欠缺准确认知,自大,没足够注意这个领域**光说不练的前提很大程度上就是自己有能力把钦点的东西都凭空发明一遍**。 106 | * 于是,自己想象什么学院派,大概**不知道自己距离真正的学院派有多远**。 107 | * 也许还死要面子不承认,不知道碰到了普通人没有机缘接触的领域,已经很凭运气了,所以在正经讨论面前连暴论到点上的机会都肆意浪费。 108 | * 还可能基于无知、无能或者态度问题,不但显示不出了解讨论的核心话题,还显示不出了解讨论对方的立场。我就不时被误解得懒得回复,这里顺带集中处理: 109 | * 比如 Haskell 之类的 PFP 党我就基于一贯相同的价值观照黑不误,不管多有技巧上的美感(其实很多是丑的)。 110 | * 而 ML 系的我也不大待见。偏偏 Robert Harper 作为黑 monad 急先锋,反而意见跟我一致。 111 | * 我还直接黑滥用等式逻辑(equational reasoning) ——这是 Haskell 之类惰性求值语言背后的大本营(讽刺的是大多数批评家根本 get 不到这点)。反正这个不会是主流,虽然能跟得上讨论问题的人是什么的基本是非主流了(对这感兴趣的大多是数学家,或者数理逻辑学家)。 112 | * 把不同的流派合并同类项是很拉仇恨的。觉得学院派就是个不学无数的垃圾筐什么杂碎都能往里面到是不是?更别提指鹿为马了。这两者加起来效果比谭浩强还要拔群。 113 | * 对没眼力见的外行来说,有点情商比较好。 114 | * 于是,拿学院派偷懒借口缩小提问的外延省心又省事,在摆脱[路径依赖](#决定性问题)、在理解非特定的高级语言设计这个专业用户都应该能实现的普遍目标的路径上,**自我放纵降点难度**,不会修正方向以至于**还在继续走弯路纠结特例**,就这样了。 115 | * 这大概也是所谓“判定递归的终止条件是需要动脑子的,但是判定循环的终止条件不需要动脑子”的真相。因为不是所有语言的思维方式都需要有这种思想钢印。 116 | * 比如 SICP 2nd Edition 入门的,只要有尾递归的概念,知道怎么把递归限制为尾递归应对这一类根本相同的算法表达需求,典型用法就不应该有什么比循环多出来的心智包袱;反而循环横竖得多记点关键字或者干脆多一层抽象。 117 | 118 |   至于就因为这里列的理由断定 PTC 只是优化手段(异常的问题单独另外说),这完全是没看 [[Cl98]](https://www.researchgate.net/profile/William_Clinger/publication/2728133_Proper_Tail_Recursion_and_Space_Efficiency/links/02e7e53624927461c8000000/Proper-Tail-Recursion-and-Space-Efficiency.pdf) 这类基础文献。PTC 之上的配置分类 Algol(如 C 的抽象机),再上面是 GC ,能说 GC 相对 Algol 只是优化手段?只要语言足够无能、语义管不动,用户吃到语言语义之外的坑(如内存占用爆炸)也活该咯? 119 | 120 |   为什么这里的问题需要澄清?因为不止是个别用户,心智包袱患者也会抱团危害社会。比如 MISRA C 强调禁止函数的递归调用,理由是为了防止栈溢出,全然不顾非递归的嵌套函数调用在典型实现中制造栈溢出也是分分钟的事(虽然说这里更离谱的是合计给你 1 字节的栈也能是 ISO C conforming implementation )。这种驯化次等从业人员的教条主义就比个别包袱危害性大多了。 121 | 122 |   当然,真有问题的地方还是存在的。比如 PTC 和析构函数(以及 Racket 的 contract check 之类)这种非尾上下文直接有对象语言语义上的冲突而需要非平凡的解决,**至今仍是没有彻底解决的问题**(之前有看见设计类型系统推断的,我觉得不大对路)。但外行么,说到底就是外行。要不是看在公开讨论并不只是会让个别人受益,不过是不是被当作嗟来之食,都纯属多说无益。 123 | 124 | ## 动态语言 125 | 126 |   类似的方式可以得到:在动态语言上添加限制使之具有静态语言的优点,比静态语言扩展动态特性(如 CLR 语言加 `dynamic` )普遍更容易实现目的。 127 | 128 |   同时,尽管是不同的性质,静态类型系统在这里算是静态语言在这里类似。进一步推论:凡是静态语言特性这样依赖阶段(phase) 的特性,都不适合出现在一般的通用语言设计中直接支持,而更适合通过扩展添加。特别地,这包含所有静态类型系统。无类型 lambda 演算扩展为有类型 lambda 演算(还不止一个方向,反过来先发明哪个就麻烦了)、JavaScript 扩展到 TypeScript 、Racket 扩展到 Typed Racket 等等,都是实际成功的例子。 129 | 130 |   单独拿出来说,是因为这说明:只讨论 JavaScript 这样的动态语言,在长期目的上(资源充足,不差钱不差人建设生态)就足够在现有一般编程语言中有代表性(尽管通用性上相比上面的描述都有缩水),而不需要再单独讨论静态语言。静态语言在通用目的语言的意义上,只是通用的动态语言一种配置或者优化,不需要单独提出特定的设计。 131 | 132 | ## 品味的判断 133 | 134 |   上面的推理还隐含着主观价值偏好:基于沉没成本厌恶,良好的品味至少和任何需要**返工**的可能最终是严格冲突的。既然一眼望得到目标,为什么要先走过头再折返呢?(注意这不是典型的最优化问题,因为即便既定目标不变,具体可优化的可操作目标具有随机性和主观性。) 135 | 136 |   在简单的情形下,折返会经过原来已经访问过的部分路径重叠,也就是和中间态相遇。这是很显然的失误决策的标志物。 137 | 138 |   偏离目标的非必要重叠对应的产出,统称**半吊子**解决方案。当然,半吊子也有合理性:因为人不总是有未来视,一眼望得到目标,曲线救国也是可以接受的。在宏观上,静态的通用目的语言(相对于动态语言)和 AOT 编译器(相对 JS 运行时这样的动态语言实现)这类都是典型的例子,虽然最终应当被取代,但在现有投入连目标的中点都遥不可及的情况下,有单独存在的意义。 139 | 140 |   也有更复杂的但习惯上直接认怂的情况,例如 GC 语言的流行不是因为 GC 语言比非 GC 语言早或者晚,也不是 GC 语言在特定情况下能牺牲内存占用和响应发挥吞吐量优势,而几乎全是因为对大部分用户普遍不理解所有权管理应当是静态设计的一部分现实这种情况的妥协。这种情况只能在路线外另行改变。于是,路线外部的交互对保持品味也是重要的——理想的品味应当有自主性和能动性,以避免被后进者拖后腿,不因后进者用户数量和直接产生的价值多少而动摇(要说大部分开发者写垃圾代码没有功劳也有苦劳,能保证比得上被他们吃掉的岗位本来能写的不那么垃圾的代码的人产生的效益么)。 141 | 142 |   这里还有个问题:万一路径上打断了怎么办?最终进展可能还不如半吊子。这的确普遍存在,属于固有风险。但统计上,如果同时试错的实例足够多,打断造成的不可利用的沉没成本期望越低(证明略)。这是为什么看似激进的 flavor 仅仅为了最终遇见的正确性也需要适当扩大生态建设的原因。风险并没减少,实质上是平摊了。但半吊子的无效产出开销会减少(证明略)。 143 | 144 | ## 例外 145 | 146 |   也有事实上非常普遍重要的性质仅因为使用上的领域特定性而需要单独提出。这里只提一个相关的:**光滑性(smoothness)** 。这种性质允许语言中的设施被**原样**地同时用于元语言和对象语言中。这是先前提出过的**降低语言设计可触及的复杂度下限**的直接技术实现手段。大部分语言完全不具有这项特征。注意这种特征普遍是语义上的,通过元编程可以转换到语法范畴中。而对应在确定语法范畴(目标语言)上的常见典型弱化是**同像性(homoiconicity)**。 147 | 148 |   光滑性猜想(rephrased, Shutt 2010):光滑性对最大化语言的抽象边界(radius of abstraction) 是必要的。 149 | 150 |   (抽象理论的形式框架略。) 151 | 152 |   考虑到语言设计过程(包含标准化的主要部分)是程序性的活动,对其自身进行元编程的描述再次应用猜想的结论,可以确保实现整个活动复杂性、使之和特定“有意义的”改动的开销接近理论下限目的。 153 | 154 | -------------------------------------------------------------------------------- /zh-CN/string-and-string-length.md: -------------------------------------------------------------------------------- 1 | # [科普]字符串和字符串的长度 2 | 3 | Created @ 2012-07-30 01:11, recovered rev 2015-09-15, markdown @ 2015-09-16. 4 | 5 | Markdown 注释:本地阅读器可附加下列内联样式替换源码中的对应内容: 6 | 7 | ```markdown 8 | NTBS ntbs 9 | NTCTS ntcts 10 | NTMBS ntmbs 11 | ``` 12 | 13 | # 首先明确几个概念 14 | 15 |   字符串:形式语言理论研究的基本对象之一,是字符的有限序列。 16 | 17 |   形式语言理论研究的基本对象之一,是字符的有限序列。 18 | 19 |   以下引用中文喂鸡“[字符串](https://zh.wikipedia.org/zh-cn/%E5%AD%97%E7%AC%A6%E4%B8%B2)”: 20 | 21 | > 设∑是叫做字母表的非空有限集合。∑的元素叫做“符号”或“字符”。在∑上的字符串(或字)是来自∑的任何有限序列。例如,如果∑ = {0, 1},则*0101*是在∑之上的字符串。\ 22 | > 字符串的长度是在字符串中字符的数目(序列的长度),它可以是任何非负整数。“空串”是在∑上的唯一的长度为0的字符串,并被指示为ε或λ。 23 | 24 |   注意,这里的长度的概念是足够清晰的。 25 | 26 |   以下引用中文喂鸡“[字符串->字符串数据类型](https://zh.wikipedia.org/zh-cn/%E5%AD%97%E7%AC%A6%E4%B8%B2#.E5.AD.97.E7.AC.A6.E4.B8.B2.E6.95.B0.E6.8D.AE.E7.B1.BB.E5.9E.8B)”: 27 | 28 | > # 字符串长度 29 | > 30 | > 尽管形式字符串可以有任意(但有限)的长度,实际语言的字符串的长度经常被限制到一个人工极大值。一般的说,有两种类型的字符串数据类型:“定长字符串”,它有固定的极大长度并且不管是否达到了这个极大值都使用同样数量的内存;和“变长字符串”,它的长度不是专断固定的并且依赖于实际的大小使用可变数量的内存。在现代编程语言中的多数字符串是变长字符串。尽管叫这个名字,所有变长字符串还是在长度上有个极限,一般的说这个极限只依赖于可获得的内存的数量。\ 31 | > …… 32 | 33 | # 表示法 34 | 35 |   一种常用的表示法是使用一个字符代码的数组,每个字符通常占用一个字节(如在 ASCII 代码中)或两个字节(如在 UCS-2 这样的 Unicode 表示中)。它的长度可以使用一个结束符(一般是 [NUL](https://zh.wikipedia.org/zh-cn/%E7%A9%BA%E5%AD%97%E7%AC%A6) ,ASCII 代码是 0 ,在C编程语言中使用这种方法)。或者在前面加入一个整数值来表示它的长度(在 Pascal 语言中使用这种方法)。 36 | 37 | 【例略】 38 | 39 |   可见字符串的长度和存储的关系是不唯一的。 40 | 41 |   在 C/C++ 中可以使用多种形式表示和存储的字符串。最常见的基本的字符串表示形式(即C标准库/C++标准库都使用的形式)通称为 C 风格字符串(C-style string) ,在 ISO C++ 的学名是 NTCTS(null terminated character string) 。 42 | 43 | **ISO C++11** 44 | 45 | > ## 17.3.17 [defns.ntcts] 46 | > 47 | > **NTCTS**\ 48 | > a sequence of values that have *character type* that precede the terminating null character type value `charT()` 49 | 50 |   具体说来,一个典型的场景是:多于一个元素的 `char`/`wchar_t`/`char16_t`/`char32_t`/其它实现允许的扩展字符类型的数组可以放一个 NTCTS 。 51 | 52 |   注意,是 a sequence of values 而不是 characters ,表示抽象的含义。下面会看到 character(但不是 multibyte character )在 C++ 标准库中的明确受限的意义。 53 | 54 |   顺便,关于 multibyte character 是 C++ 整体通用的基本术语之一,所以独立于 character 之外考虑: 55 | 56 | **ISO C++11** 57 | 58 | > ## 1.3.13 [defns.multibyte] 59 | > 60 | > **multibyte character**\ 61 | > sequence of one or more bytes representing a member of the extended character set of either the source or the execution environment\ 62 | > [ *Note:* The extended character set is a superset of the basic character set (2.3).*—end note* ] 63 | 64 |   (至于字符、基本执行字符集什么的虽然是必要基础但理解起来很简单,暂且不在此展开。) 65 | 66 | **ISO C++11** 67 | 68 | 69 | > ##### 17.5.2.1.4 Character sequences [character.seq] 70 | > 71 | > 1 The C standard library makes widespread use of characters and character sequences that follow a few uniform conventions:\ 72 | > — A *letter* is any of the 26 lowercase or 26 uppercase letters in the basic execution character set.167\ 73 | > — The *decimal-point character* is the (single-byte) character used by functions that convert between a (single-byte) character sequence and a value of one of the floating-point types. It is used in the character sequence to denote the beginning of a fractional part. It is represented in Clauses 18 through 30 and Annex D by a period, ’.’, which is also its value in the `"C"` locale, but may change during program execution by a call to `setlocale(int, const char*)`,168 or by a change to a locale object, as described in Clauses 22.3 and 27.\ 74 | > — A *character sequence* is an array object (8.3.4) *A* that can be declared as *T A [N]*, where *T* is any of the types `char`, `unsigned char`, or `signed char` (3.9.1), optionally qualified by any combination of const or volatile. The initial elements of the array have defined contents up to and including an element determined by some predicate. A character sequence can be designated by a pointer value *S* that points to its first element.\ 75 | > 167) Note that this definition differs from the definition in ISO C 7.1.1.\ 76 | > 168) declared in `` (22.6). 77 | 78 |   据此可以定义更具体的 NTBS(null terminated byte string) : 79 | 80 | **ISO C++11** 81 | 82 | > ##### 17.5.2.1.4.1 Byte strings [byte.strings] 83 | > 84 | > 1 A *null-terminated byte string*, or NTBS, is a character sequence whose highest-addressed element with defined content has the value zero (the *terminating null* character); no other element in the sequence has the value zero.169\ 85 | > 2 The *length* of an NTBS is the number of elements that precede the terminating null character. An empty ntbs has a length of zero.\ 86 | > 3 The *value* of an NTBS is the sequence of values of the elements up to and including the terminating null character.\ 87 | > 4 A *static* NTBS is an NTBS with static storage duration.170\ 88 | > 169) Many of the objects manipulated by function signatures declared in \ (21.7) are character sequences or NTBSs. The size of some of these character sequences is limited by a length value, maintained separately from the character sequence.\ 89 | > 170) A string literal, such as "abc", is a static ntbs. 90 | 91 |   NTBS 的元素通常用 `char` 类型对象或值表示。 92 | 93 |   NTBS 在 NTCTS 和 character sequence 的基础上明确了存储。此外,NTBS 区分于 NTCTS 的定义的重要目的之一是为了明确(允许变长编码的)多字节字符串 NTMBS 的外延——注意,这里的一些“长度”开始体现出显著的区别。 94 | 95 |   先看定义: 96 | 97 | **ISO C++11** 98 | 99 | > ##### 17.5.2.1.4.2 Multibyte strings [multibyte.strings] 100 | > 101 | > 1 A *null-terminated multibyte string*, or NTMBS, is an NTBS that constitutes a sequence of valid multibyte characters, beginning and ending in the initial shift state.171\ 102 | > 2 A *static* NTMBS is an NTMBS with static storage duration.\ 103 | > 171) An NTBS that contains characters only from the basic execution character set is also an NTMBS. Each multibyte character then consists of a single byte. 104 | 105 |   可见 NTMBS 是 NTBS 的子集,它其中可以包含多个(连续)字节组成的字符。 106 | 107 |   按 17.5.2.1.4.1/2 ,NTMBS 即 NTBS 的长度是其中包含的元素数。这里的“元素”概念和NTBS中有区别,即强调作为 NTMBS 时长度是多字节字符数而不是字符(字节)数。显然对于一般的NTMBS,即便去除结尾的空字符,长度和占用的字节数可以不同。 108 | 109 |   但是,ISO C 里面关于“长度”可以有些关键性的不同。简而言之,ISO C标准库使用的string相当于 NTBS ,类似 NTMBS 的概念中长度仍以字节计: 110 | 111 | **ISO C99/C11(N1570)** 112 | 113 | > 7.1.1/1 A *string* is a contiguous sequence of characters terminated by and including the first null character. The term *multibyte string* is sometimes used instead to emphasize special processing given to multibyte characters contained in the string or to avoid confusion with a wide string. A *pointer to a string* is a pointer to its initial (lowest addressed) character. The *length of a string* is the number of bytes preceding the null character and the *value of a string* is the sequence of the values of the contained characters, in order. 114 | 115 |   至于宽字符串,ISO C 单独处理(当然长度也是),某种意义上从 C 角度来说“宽字符串不是字符串”: 116 | 117 | **ISO C99/C11(N1570)** 118 | 119 | > ## 7.1.1 Definitions of terms 120 | > 121 | > 4 A *wide string* is a contiguous sequence of wide characters terminated by and including the first null wide character. A *pointer to a wide string* is a pointer to its initial (lowest addressed) wide character. The *length of a wide string* is the number of wide characters preceding the null wide character and the *value of a wide string* is the sequence of code values of the contained wide characters, in order. 122 | 123 |   这样对于 `strlen`/`wcslen` 这样的接口来说,所求的长度是非常容易理解的——不包含末尾空字符的字符数;乘上 `sizeof(char)`/`sizeof(wchar_t)` 就是对应不包含末尾空字符的字节数(对于char来说自然可以互换)。 124 | 125 |   对于多字节字符串,`strlen` ( `string::size`/`string::length` 也一样,反正都不知道编码)的含义就不那么直观了。从和形式抽象相容的定义来讲,NTMBS是恰当的;而NTBS的“长度”只是在多字节表示下的字节数而非字符数。 126 | 127 |   大概是因为习惯和历史原因,ISO C/C++ 在这里的一致性上做得不太雅观。不过,在未强调编码/字符集前只考虑空字符前的字节数也是可以理解的。 128 | 129 | # 字符串字面量 130 | 131 |   在 C/C++ 中,还有一个非常被错误理解的特性:字符串字面量(string literal) 。 132 | 133 |   重点/易错点择要: 134 | 135 | ## 1.词法形式 136 | 137 |   `"xxx"` 、`L"xxx"` 、`u"xxx"` 等。另外 raw string literal/user defined literal 以及后者造成的 C++03/C++11 关于一个string literal是否是一个 *token* 的不兼容性等等,这里关系不大,先略过。 138 | 139 | ## 2.具有静态存储期 140 | 141 | ## 3.除了 `u` 等起始的 Unicode 字面量,使用的字符集由实现定义(通常由源文件的编码决定) 142 | 143 |   因此事实上除了仅包含基本执行字符集中的元素的字符串字面量,相同的输入并不一定导致相同的结果,长度也是由实现定义的。 144 | 145 | ## 4.末尾隐含空字符 146 | 147 | ## 5.类型 148 | 149 |   对于 C ,类型是对应字符类型的数组类型;对于 C++ ,类型是对应字符类型的 `const` 数组类型。 150 | 151 | ## 6.修改引起未定义行为 152 | 153 | ## 7.**不等同于字符串** 154 | 155 |   一个字符串字面量不一定表示一个字符串。 156 | 157 |   关于最后一点在 ISO C 里有特别指出: 158 | 159 | **ISO C11(N1570)** 160 | 161 | > ## 6.4.5 String literals 162 | > 163 | > 6 In translation phase 7, a byte or code of value zero is appended to each multibyte character sequence that results from a string literal or literals.78) ...\ 164 | > 78) A string literal need not be a string (see 7.1.1), because a null character may be embedded in it by a `\0` escape sequence. 165 | 166 |   例如,`"123\0\0123"` 不是一个字符串,而是两个连续存储的字符串(注意后面的 `\0` 不是一整个字符而是和后面的字符继续构成八进制转义序列)。 167 | 168 |   除了C风格字符串,还有其它形式。ISO C++ 标准库提供了类模版 `std::basic_string` 。对于 C++03 ,它不一定以 NTCTS 的形式存储;而对于 C++11 ,因为接口的限制,它的实例对象在可观察行为上表现为内部存储一个NTCTS。 169 | 170 |   非标准库的形式也有很多。比如说 M$ 的 `_bstr_t` ;或者自己实现一个类 PASCAL 字符串,等等。 171 | 172 |   对于这类非标准字符串,如果涉及到多字节字符且不明确实现(通常来说对于用户不应该明确实现)的话,通常应当约定“长度”指的是字符数还是字节数。 173 | 174 |   至于数组长度,完全不上道的玩意儿。 175 | 176 |   也就是字符串字面量用来初始化数组约定隐式数组的长,和C风格字符串都没什么直接关系。 177 | 178 | # 新手指引 179 | 180 | Appended @ 2012-7-30 01:32. 181 | 182 | * 明确一般意义形式上的字符串和 C 风格字符串的差异; 183 | * 明确 C 风格字符串和字符串字面量的差异; 184 | * 明确空字符不是字符串的内容; 185 | * 明确一般意义上的字符串长度和字符集/编码相关; 186 | * 明确 C 风格字符串的长度通常只讨论不包括末尾空字符的字节数。 187 | 188 |   剩下基本问题不大了。 189 | 190 | # 非标准库字符串进阶参考 191 | 192 | Appended @ 2012-7-30 01:39. 193 | 194 | * [C++1y 候选的 `string_ref`](https://code.google.com/p/cxx1y-array-string-ref/source) ; 195 | * [LLVM 中的各种字符串表示形式的实现](https://llvm.org/docs/ProgrammersManual.html#string_apis、llvm.org/docs/ProgrammersManual.html#ds_string) 。 196 | 197 | -------------------------------------------------------------------------------- /zh-CN/terms-and-bibliography.md: -------------------------------------------------------------------------------- 1 | # 术语和文献列表 2 | 3 | 本文用于对相关文章内容的正式引用提供解释性补充参照。 4 | 5 | ## C 语言标准 6 | 7 | ANSI C89 指 ANSI X3.159-1989 ,后来被采纳为 ISO/IEC 9899:1990 ,通称 C90 。两者正文仅有格式变化(另外 C90 不包含 Rationale )。 8 | 9 | C99 或 ISO C99 指 ISO/IEC 9899:1999 ,被 ANSI 于 2000 年 5 月采纳。在草案阶段被称为 C9X 。 10 | 11 | [C11](https://zh.wikipedia.org/zh-cn/C11) 或 ISO C11 指 ISO/IEC 9899:2011 ,是第三版正式 C 语言国际语言标准,也被称为 C1X 。 12 | 13 | C17 、C18 或 ISO C18 指 ISO/IEC 9899:2018 ,是第四版正式 C 语言国际语言标准,对上一版只有小的修正。草案定稿于 2017 年,出版于 2018 年。其中 `__STDC_VERSION__` 定义为 `201711L` 。 14 | 15 | [ANSI C](https://zh.wikipedia.org/zh-cn/ANSI_C) 一般即 ANSI C89 。实际上也只有这版标准是先 ANSI 后 ISO 。 16 | 17 | ## C++ 标准 18 | 19 | C++98 或 ISO C++98 指 ISO/IEC 14882:1998 ,是第一个 C++ 语言国际标准。 20 | 21 | [C++03](https://en.wikipedia.org/wiki/C++03) 或 ISO C++03 指 ISO/IEC 14882:2003 ,是第二版 C++ 语言国际标准,对上一版只有小的修正。 22 | 23 | [C++11](https://zh.wikipedia.org/zh-cn/C++11) 或 ISO C++11 指 ISO/IEC 14882:2011 ,是第三版 C++ 语言国际标准,有较大改动。在出版之前,曾经有较长时间的延期,被称为 C++0x 。 24 | 25 | [C++14](https://zh.wikipedia.org/zh-cn/C++14) 或 ISO C++14 指 ISO/IEC 14882:2014 ,是第四版 C++ 语言国际标准,有一定修正,之前称为 C++1y 。 26 | 27 | [C++17](https://en.wikipedia.org/wiki/C++17) 或 ISO C++17 指 ISO/IEC 14882:2017 ,是第五版 C++ 语言国际标准,有较大改动,之前称为 C++1z 。 28 | 29 | [C++20](https://en.wikipedia.org/wiki/C++20) 或 ISO C++20 指预定出版的 ISO/IEC 14882:2020 ,是 C++ 语言国际标准的一个主要版本,被称为 C++2a 。 30 | -------------------------------------------------------------------------------- /zh-CN/variables.md: -------------------------------------------------------------------------------- 1 | # [科普]变量、全局变量及其它 2 | 3 |   本文试图对各种蛋疼的所谓“全局”“变量”问题的解释做个小结。 4 | 5 | # 变量(variable) 的一般含义 6 | 7 |   “变量”来源于代数学,是数学中最伟大的发明之一。变量是表示可变数学对象的符号(symbol) 。它具有两重含义,一是指在某个上下文中的符号本身;二是这个符号表示的可变的值(value) 。 8 | 9 | # 程序设计语言中的变量、变量名(variable name) 和作用域(scope) 10 | 11 |   在程序设计语言中也有类似的概念。不同的是,一般需要更加明确地指出一个变量有效的上下文。这个上下文通常是代码中的一段(连续的)区域(region) ,称为作用域(scope) 。只有在作用域中使用的变量是有效的。 12 | 13 |   由于作用域针对的是一段代码区域,所以对于变量的两重含义的约束是不同的。作用域实际只限定变量作为符号本身,也就是字面形式,即变量名(variable name) 。在语言的语法中,通常标识符(identifier) 这一成分就可以作为变量名。当然变量名也可以是更复杂的表达式。 14 | 15 |   而变量的另一种含义,即变量的内容,各种语言可以提供不同的抽象,例如可以直接指定存储,也可以约定使用其它抽象的语义形式如何体现可变的状态。 16 | 17 |   另外,在明确了名称的概念之后,作用域也不只限于变量,对于任何其它具有名称的抽象也适用。作用域能避免相同的名称的冲突,也就是说允许相同的名称在不同的上下文中指称(denotes) 不同的内容。 18 | 19 |   对 ISO/IEC JTC1/SC22 标准化的语言,通常会把 ISO/IEC 2382 作为正式引用(normative reference) 。这是术语定义的集合,其中就有“变量”。[ISO/IEC 2382:2015](https://www.iso.org/obp/ui/#iso:std:iso-iec:2382:ed-1:v1:en) 给出的 variable 的一般定义如下: 20 | 21 | > **variable**\ 22 | > quadruple, established by a declaration or an implicit declaration, that consists of an identifier, a set of data attributes, one or more addresses, and data values, where the relationship between the addresses and the data values may vary 23 | 24 | 其中引用的其它相关术语的定义如下: 25 | 26 | > **address**\ 27 | > value that identifies a location\ 28 | > **declaration**\ 29 | > explicit language construct that introduces one or more identifiers into a program and specifies how these identifiers are to be interpreted\ 30 | > **implicit declaration**\ 31 | > declaration caused by the occurrence of an identifier that designates an object, whose characteristics are determined by default 32 | 33 | 可见和上面的一般说法是基本一致的。 34 | 35 | # 限定名称(qualified name) 36 | 37 |   上面提到过使用作用域的一个原因是防止相同名称的冲突。有时作用域限制不太方便,因为需要在整个程序范围内访问的名称都得放在全局作用域中。所以还需要其它的机制。 38 | 39 |   许多语言提供了全局名称的额外组织机制,一个比较通用的手段是允许对标识符增加表示作用域子集的前缀构成限定名称。限定名称更加明确,而对应未被限定的名称则可以适合更多作用域。 40 | 41 |   这种机制可以有不同的表现形式,如 C++ 和 C# 的命名空间(namespace) ,Ada 和 Java 的包(pakage) 等(Java 首先把变量放在类中,然后以类作为包的成员)。 42 | 43 | # C 语言的变量 44 | 45 |   应该指出,K&R C 提到了 variable ,而 ISO C 中没有正式定义这个概念,甚至 ISO C 正式文本(直到 ISO C17)中正式(normative) 用到的 variable 一词也都不是变量的意思,而是如 variable length array 等。 46 | 47 |   这里的变量可以按传统意义理解,即包括变量名在内,但对于 C 这样的明确支持不同作用域的语言来说,在整个程序范围内变量的同一性(identity) 就成了问题——“变量名一致的变量就是同一个变量”有违整体上的直觉。而要是撇开造成问题的变量名,即专指变量内容,在 C 语言中使用表示存储的对象(object) 就能很好地解释清楚了,没必要用“变量”这个词来增加理解上的困难。不知是不是这个原因,ISO C 才没定义变量的概念。 48 | 49 |   也就是说,对变量的概念存在两种理解,不见得哪种就是对的,而且都没有必要。这种二义性已经使得一些基本问题的讨论变得没有意义,如“变量是不是表达式”。由于讨论语言问题时“变量”可以被更清晰的术语取代,因此,可以回避这个模糊的说法。 50 | 51 |   除了说明性(informative) 的(如出现在 NOTE 和脚注中的)用法,自 ISO/IEC 9899:2017 引入的 名词 variable 的其它用法包括: 52 | 53 | * 条件变量([condition variable](https://zh.wikipedia.org/zh-cn/%E7%9B%A3%E8%A6%96%E5%99%A8_%28%E7%A8%8B%E5%BA%8F%E5%90%8C%E6%AD%A5%E5%8C%96%29#%E6%A2%9D%E4%BB%B6%E8%AE%8A%E6%95%B8%28Condition_Variable%29)) ,和这里的所谓变量是两回事。 54 | * 7.17.2.1 `ATOMIC_VAR_INIT` 宏中出现的应为 object 的 variable(可以看到这个概念在 C++ 中明确有定义),这里明显是抄错了 ISO C++11 里的文本漏了替换。(说起来这个都已经[被提案在 ISO C++20 中废弃了](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1138r0.pdf),还有 [C 的问题的一份功劳](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2257.htm#dr_485)……) 55 | 56 | # C++ 语言的变量 57 | 58 |   C++ 中的变量通过对象或不是作为类的非静态成员的引用的声明引入。变量名指称被声明的引用或对象: 59 | 60 | **ISO C++11** 61 | 62 | > **3**/6 A variable is introduced by the declaration of a reference other than a non-static data member or of an object. The variable’s name denotes the reference or object. 63 | 64 |   由于引用的存在,这里变量的概念并没有被对象等直接取代。 65 | 66 | # 全局(global) 67 | 68 |   全局是指整个程序的范围。例如,对于运行时来说,全局状态是程序的各个部分都能访问的状态。 69 | 70 |   全局变量(global variable) 是指程序代码中的各个作用域都能访问的变量。但是,下面会看到,C/C++代码无法真正地达到这点。 71 | 72 |   与全局对立的是局部(local) 。与全局变量对立的是局部变量(local variable) 。但是,C/C++程序中,局部变量往往指块作用域(block scope) 中的变量,并不严格对立。因此下文不使用这些概念。 73 | 74 | # C 语言的作用域和名称空间 75 | 76 |   C语言有且仅有函数作用域(function scope) 、文件作用域(file scope) 、块作用域和函数原型作用域(function prototype scope) 这些作用域: 77 | 78 | ISO C11(N1570) 79 | 80 | > # 6.2.1 Scopes of identifiers 81 | > 82 | > 3 A label name is the only kind of identifier that has function scope. It can be used (in a goto statement) anywhere in the function in which it appears, and is declared implicitly by its syntactic appearance (followed by a : and a statement).\ 83 | > 4 Every other identifier has scope determined by the placement of its declaration (in a declarator or type specifier). If the declarator or type specifier that declares the identifier appears outside of any block or list of parameters, the identifier has file scope, which terminates at the end of the translation unit. If the declarator or type specifier that declares the identifier appears inside a block or within the list of parameter declarations in a function definition, the identifier has block scope, which terminates at the end of the associated block. If the declarator or type specifier that declares the identifier appears within the list of parameter declarations in a function prototype (not part of a function definition), the identifier has function prototype scope, which terminates at the end of the function declarator. If an identifier designates two different entities in the same name space, the scopes might overlap. If so, the scope of one entity (the inner scope) will end strictly before the scope of the other entity (the outer scope). Within the inner scope, the identifier designates the entity declared in the inner scope; the entity declared in the outer scope is hidden (and not visible) within the inner scope. 84 | 85 |   顺带纠正两点常见误区: 86 | 87 | * a)函数体内中声明的名称所在的作用域是块作用域,而不是函数作用域,后者是标号(label) 专有的(这条也适用于C++)。 88 | * b)没有所谓的全局作用域,通常所谓的全局充其量只是在文件作用域而已。 89 | 90 |   不过在C语言中同一作用域其实允许有限地允许不同实体重名。除了限定名称外,C 语言还有一种消除歧义的方式——名称空间(name space) : 91 | 92 | **ISO C11(N1570)** 93 | 94 | > # 6.2.3 Name spaces of identifiers 95 | > 96 | > 1 If more than one declaration of a particular identifier is visible at any point in a translation unit, the syntactic context disambiguates uses that refer to different entities. Thus, there are separate name spaces for various categories of identifiers, as follows:\ 97 | > — label names (disambiguated by the syntax of the label declaration and use);\ 98 | > — the tags of structures, unions, and enumerations (disambiguated by following any32) of the keywords struct, union, or enum);\ 99 | > — the members of structures or unions; each structure or union has a separate name space for its members (disambiguated by the type of the expression used to access the member via the . or -> operator);\ 100 | > — all other identifiers, called ordinary identifiers (declared in ordinary declarators or as enumeration constants).\ 101 | > 32) There is only one name space for tags even though three are possible. 102 | 103 |   C语言没有限定名称,所有的名称都是标识符。ISO C也没有直接说明“名称”的含义,但这里应该是清楚的。(实际上,C语言语法中的identifier在其前身B语言中的对应物就叫 name 。) 104 | 105 | # C++ 语言的作用域 106 | 107 |   C++的作用域比较多,以下以标准文本的标题排列: 108 | 109 | **ISO C++11** 110 | 111 | > 3.3.3 Block scope [basic.scope.local]\ 112 | > 3.3.4 Function prototype scope [basic.scope.proto]\ 113 | > 3.3.5 Function scope [basic.funscope]\ 114 | > 3.3.6 Namespace scope [basic.scope.namespace]\ 115 | > 3.3.7 Class scope [basic.scope.class]\ 116 | > 3.3.8 Enumeration scope [basic.scope.enum]\ 117 | > 3.3.9 Template parameter scope [basic.scope.temp] 118 | 119 |   注意到块作用域一节的交叉引用(cross reference) 的标签(label) 中说的是“local”。这是因为 C++98/03 中用词比较混乱,有时候作 block scope 有时候作 local scope(局部作用域),到 C++11 按 ISO C 统一了,但交叉引用在没有整节作废时还是需要保持兼容性,所以没变。 120 | 121 |   C++ 的名称是指标识符或限定标识符的使用: 122 | 123 | **ISO C++11** 124 | 125 | > **3**/4 A name is a use of an identifier (2.11), operator-function-id (13.5), literal-operator-id (13.5.8), conversion-function-id (12.3.2), or template-id (14.2) that denotes an entity or label (6.6.4, 6.1). 126 | 127 |   关于“全局”,这里需要提一点: 128 | 129 | **ISO C++11** 130 | 131 | > **3.3.6**/3 The outermost declarative region of a translation unit is also a namespace, called the global namespace. A name declared in the global namespace has global namespace scope (also called global scope). The potential scope of such a name begins at its point of declaration (3.3.2) and ends at the end of the translation unit that is its declarative region. Names with global namespace scope are said to be global name. 132 | 133 |   也就是说 C++ 有“全局(命名空间)作用域”,是命名空间作用域的特例,其中名称为全局名称。可以看出它和C的文件作用域是对应的。 134 | 135 |   (为什么 C 没有真正的“全局”而 C++ 可以有了呢——并不是这个单独决定的。) 136 | 137 | # C/C++ 程序与链接(linkage) 138 | 139 |   一个C/C++程序由一个或多个翻译单元(translation unit) 组成。翻译单元作为源代码可以各自独立地被翻译为目标代码然后链接(linking) 成完整的程序。语法上,(预处理后的、正确的)翻译单元由名称的声明(declaration) 构成。声明引入标识符/名称并确定它们的指称。某个翻译单元中的声明仅在这个翻译单元内有效。合法的程序中使用任意名称之前都需要这个名称在所在翻译单元内(而不是“全局”)的(用户提供的,或者实现预定义的)声明。 140 | 141 |   翻译单元的地位是等价的;不存在“全局”的翻译单元。因此,一般地,C/C++ 程序不存在源码层次上的“全局”的概念:“任意名称”自然也包括变量名,只能依赖于具体翻译单元。这样,不管是 C 还是 C++ ,都无所谓真正意义上的“全局变量”。(考虑到 C90 允许隐式声明,倒是有些“全局”的风格;但这种手法不被当前的 C/C++ 接受。) 142 | 143 |   但语言允许在不同翻译单元中共享程序的全局状态——可以直接通过共享不同翻译单元的实体的指称实现。只要指定不同翻译单元中文件/全局作用域中的某些名称指称相同的对象就可以达到“全局变量”的目的。相应地,也存在和外部无关,只是指称翻译单元内部存在的实体名称。这样的属性称为链接(linkage) 。注意链接是针对名称的,而不是对象等具体实体。 144 | 145 |   很自然地,能以相同名称共享外部实体的名称具有外部链接(external linkage) ,只在翻译单元内部共享指称实体的具有内部链接(interal linkage) ,不共享指称的无链接(no linkeage) 。 146 | 147 |   所谓外部变量(external variable) 就是指变量名具有外部链接的变量。这和作用域没有直接的关系。但由于对象声明默认具有的链接,容易造成混淆。这里仅举两例,不详细展开:a) `extern` 的确切含义; b)`const`对象在 C 和 C++ 中具有不同的链接。 148 | 149 |   这样,可以确定,外部变量和“全局变量”不是一回事。这方面的误解看来还是不少,不知道拜谁所赐了。 150 | 151 | # 结论:C/C++ 中“全局变量”的确切含义 152 | 153 |   可见,无论是“全局”还是“变量”,在 C 和 C++ 之间都有一些差距。 154 | 155 |   所谓“全局”,无论在 C/C++ 中,都没有传统的意义。C 能实现在效果上和“全局变量”类似的只是名称具有外部链接的对象,但硬说“全局”就名不副实了。C++ 多了全局命名空间这种在源码层次上强制的约定,但要真正能保证实现“全局”,还是得靠链接。 156 | 157 |   C 语言中讨论“全局变量”可以有各种没明确的意义,所以这个混乱的概念还是不用为妙。只有C++中是可以明确的:指全局命名空间作用域中的变量。“文件作用域对象”才是C语言中对应的比较明确的提法。 158 | 159 |   C 的这种混乱的根源除了 ISO C 没有明确一些和传统认知有微妙差异的关键概念外,主要是由于缺乏对通过链接共享实体指称的多翻译单元的语言设计的理解所致。不过说到底,既然是坑,没能力填上就绕过去吧。 160 | 161 | # 补充观点 162 | 163 |   关于更一般问题的来源的讨论,可以参照[这里](authority-and-confusion.md)。 164 | 165 | -------------------------------------------------------------------------------- /zh-CN/what-is-syntax.md: -------------------------------------------------------------------------------- 1 | # [高级点的技术交流]什么叫语法(syntax) 2 | 3 | Created @ 2015-07-01 08:50. 4 | 5 | 似乎以前发的主题都搜不到几个,所以窃点精。 6 | 7 | 先声明,我不是撸这专业的;然而大都数吹逼材料在这个问题上的犯傻过于普遍,都不到点上,太不像样了,以至于熟练工还真能一本正经口胡…… 8 | 9 | 原因大概是从头就错惯了,以讹传讹(就像什么“变量”“堆栈”的意思一样)。 10 | 11 | # 0.体例 12 | 13 | 引文文本使用 `>` 前缀(隐含在 markdown 源代码中自动转换)。 14 | 15 | 包括翻译的引文用 `[[ ]]` 标识。 16 | 17 | # 1.词义 18 | 19 | 中文维基被墙所以照搬英文维基解释翻译一下。 20 | 21 | 词条 [syntax](https://en.wikipedia.org/wiki/Syntax) : 22 | 23 | [[ 24 | 25 | > In linguistics, syntax is the set of rules, principles, and processes that govern the structure of sentences in a given language. The term syntax is also used to refer to the study of such principles and processes. In linguistics, syntax is the set of rules, principles, and processes that govern the structure of sentences in a given language. The term syntax is also used to refer to the study of such principles and processes. The goal of many syntacticians is to discover the syntactic rules common to all languages. 26 | 27 | 在语言学中,语法是一个语言中控制句子结构中的规则、原理和过程。术语 syntax 也指对这些原理和过程的研究。(译注:syntax = 语法学。)许多语法学家的目标是发现适用于所有语言的语法规则。 28 | 29 | ]] 30 | 31 | [[ 32 | 33 | > In mathematics, syntax refers to the rules governing the behavior of mathematical systems, such as formal languages used in logic. (See logical syntax.) 34 | 35 | 在数学中,语法指控制数学系统行为的规则,例如逻辑学使用的形式语言。(参见逻辑语法。) 36 | 37 | ]] 38 | 39 | [[ 40 | 41 | > ## Etymology 42 | > 43 | > From Ancient Greek: σύνταξις "coordination" from σύν syn, "together," and τάξις táxis, "an ordering". 44 | 45 | ## 语源 46 | 47 | 古希腊文 σύνταξις “协调”,由词素 σύν(syn) “在一起” 和 τάξις táxis “一种秩序”组成。 48 | 49 | ]] 50 | 51 | 语法学的历史非常悠久,公元前 4 世纪古印度语法学家波你尼(Pāṇini) 撰写的梵语语法《八章书》(Aṣṭādhyāyī) 是一个重要的代表……余略…… 52 | 53 | 事实上,抛开自然语言的历史,词条 [syntax (logic)](https://en.wikipedia.org/wiki/Syntax_%28logic%29) 给出了更贴近这里要讨论的上下文的定义: 54 | 55 | [[ 56 | 57 | > In logic, syntax is anything having to do with formal languages or formal systems without regard to any interpretation or meaning given to them. Syntax is concerned with the rules used for constructing, or transforming the symbols and words of a language, as contrasted with the semantics of a language which is concerned with its meaning. 58 | 59 | 在逻辑学中,语法是任何形式语言或形式系统中不考虑任何解释或含义的部分。语法和用于构造或转换一个语言中的符号和词的规则有关,这和语言的语义——和含义相关——相反。 60 | 61 | ]] 62 | 63 | [[ 64 | 65 | > The symbols, formulas, systems, theorems, proofs, and interpretations expressed in formal languages are syntactic entities whose properties may be studied without regard to any meaning they may be given, and, in fact, need not be given any. 66 | 67 | 在形式语言中表达的符号、公式、系统、定理、证明和解释是语法上的实体,它们的属性可以在不考虑解释和含义的情况下被研究——事实上也不需要。 68 | 69 | ]] 70 | 71 | [[ 72 | 73 | > Syntax is usually associated with the rules (or grammar) governing the composition of texts in a formal language that constitute the well-formed formulas of a formal system. 74 | 75 | 语法通常和控制形式语言中决定合式公式的文本的构造的规则(或文法)关联。 76 | 77 | ]] 78 | 79 | [[ 80 | 81 | > In computer science, the term syntax refers to the rules governing the composition of meaningful texts in a formal language, such as a programming language, that is, those texts for which it makes sense to define the semantics or meaning, or otherwise provide an interpretation. 82 | 83 | 在计算机科学中,术语“语法”指在形式语言中控制构造有意义的文本的规则,例如对一个程序语言,即明确对定义语义或含义有意义或提供一个解释的文本。 84 | 85 | ]] 86 | 87 | 关于形式语言的语法学,实际上是现代数学基础最重要的分支之一(数理逻辑)的主要组成部分。对此的延伸话题限于篇幅不作展开,有兴趣的读者可以查找词条内的链接和参考文献进一步了解术语之间的联系。 88 | 89 | # 2.文法(grammar) 90 | 91 | 细心的读者可能会发现,实际上经常被称为语法的词语并不是指 syntax ,而是上面出现过的另一个容易混淆的词:grammar 。 92 | 93 | 仅涉及自然语言的话题中,grammar 常被翻译成“语法”,而 syntax 则是“句法”(上面的 logical syntax更惯于被翻译成“逻辑句法”),这并没有造成太大问题。不过在涉及更广的范围下,这样会造成一些误会。因为“句”的概念的微妙差异,导致“句法”这个词失去基础。这个情况下,作为泛指,通译 grammar 为“文法”,syntax 为“语法”。 94 | 95 | 按[维基](https://en.wikipedia.org/wiki/Grammar)给的定义: 96 | 97 | [[ 98 | 99 | > In linguistics, grammar is the set of structural rules governing the composition of clauses, phrases, and words in any given natural language. The term refers also to the study of such rules, and this field includes morphology, syntax, and phonology, often complemented by phonetics, semantics, and pragmatics. 100 | 101 | 在语言学中,文法是指任意一个自然语言中控制组合分句、词组和词结构性规则的集合。这个术语(译注:语法学)也指对这些规则的研究,这个领域包括(词语)形态学、语法学和音韵学(译注:不要和后面讲的 phonetics 混淆),经常也涉及语音学、语义学和语用学。 102 | 103 | ]] 104 | 105 | 可见,至少在自然语言中,文法学的研究范围涵盖了语法学;作为研究对象,语法现象是文法现象的真子集。 106 | 107 | 对于形式语言来说,没有系统的音韵学或语音学研究的必要。但是,其它重要的部分,尤其是语义和语用这样明确不属于语法学研究范围的内容,仍然是重要的文法现象。 108 | 109 | 因此,随意混淆“语法”和“文法”是有问题的。在文法的范围内偷换“语法”是典型的缩小外延的逻辑谬误。 110 | 111 | # 3.语义(semantics) 112 | 113 | 上面的讨论实际上还省略了一个重要的问题:什么是“含义”(meaning) ,和“语义”又有哪些差异? 114 | 115 | 这里一并澄清关键点:前者泛指所要表达的内容即“目的”,而后者是指对含义的一般研究,尤其侧重和被使用的语言的联系。 116 | 117 | 提这个问题是因为设计和使用人工语言需要注意的一些现象。人工语言的语法并非自然演化而是人为设计的,仅从动机来讲语法背后往往存在非常具体的含义。但是,这些语言的语义和设计这些语言的语法时已经被考虑并集成在语法规则内部的含义并没有关系。 118 | 119 | 这样的一个明显好处是,语义规则可以完全和语法规则分离讨论(不像自然语言反复折腾上千年也不会有完全的标准形式);对于用机器实现语言,语法和语义规则的检查(前者又称为分析(parsing) )也能够容易被分离,便于复用。 120 | 121 | 因为缺乏设计和实现语言的机会和必要训练,这个和自然语言不同的特性往往造成不少初学者几乎永远都没有机会搞清楚问题,而对解决一些问题(例如学习新的程序设计语言)造成一些阻碍。 122 | 123 | 这里的设计要点是有普遍实用意义的。形式文法可以同时描述语法规则和语义规则,但相比之下不管是理论还是实践前者远远更加成熟。具有文法表达的形式语义通常是指称语义或公理语义,这对于大多数语言设计者来说要求过高,且现阶段因为各种局限性仍然缺乏实用价值;所以大多数语言规范并不会给出形式文法指定的形式语义。若在设计时不对语义和非语义部分加以区分,则实现语言更加混乱和困难。 124 | 125 | 与形式语义的困难相反,基本上任何现在的人工语言都会给出形式语法——一般通过 BNF 范式之类的领域特定元语言来描述。这种形式描述的主要优势是能够对处理语法的系统(主要就是语法分析器)提供自动化实现,例如分析器生成器(经典的如 yacc )或分析器组合器(如 Parsec )。因为可以节约大量编写和维护分析器的工作,这些自动化工具为调整语言的语法设计带来了极大的便利。(当然,并不是所有语言都方便这样做,下面的例子会提到。) 126 | 127 | 由于形式语法的高度普及,有时候提到syntax就指得是形式语法,而其它构造规则可能并不称为语法——尽管它们部分或全部地是传统语法学研究范围的内容。 128 | 129 | # 4.词法(lexical syntax/lexical grammar) 130 | 131 | 在语法中,因为有些最基本的和字符序列直接相关的部分内容的含义相对固定,而不需要特别指定不同的规则,习惯上把这部分语法单独提出称为词法。 132 | 133 | 在实现上,词法分析和其它更一般的语法分析也可以分离并单独配置;同时,也便于学习者理解语言的语法构造。实践证明这是自然的,因此这种惯例普遍存在。 134 | 135 | 这样,也可以提升词法到和其它一般语法并列,直接使用文法描述。 136 | 137 | 当然也不是所有语言都必要单独提出词法(除了指定允许什么字符以外),比如brainf*ck或者组合子逻辑以及这样的语法上异常“简单”的语言。 138 | 139 | # 5.实例 140 | 141 | 在 ISO C Clause 6 中,语言的形式语法在标题 **Syntax** 下澄清,而剩余部分澄清非形式的翻译时检查的约束条件 **Constraints** 和一般的语义规则 **Semantics** (对于库还有 **Runtime Constraints** )。 142 | 143 | ISO C 区分 **Constraints** 的要点是——这些规则和 **Syntax** 一样,必须在翻译时被检查和诊断(diagnostics)。 144 | 145 | **WG14/N1570** 146 | 147 | > # 4 Conformance 148 | > 149 | > 2 If a ‘‘shall’’ or ‘‘shall not’’ requirement that appears outside of a constraint or runtimeconstraint is violated, the behavior is undefined. Undefined behavior is otherwise\ 150 | indicated in this International Standard by the words ‘‘undefined behavior’’ or by the 151 | omission of any explicit definition of behavior. There is no difference in emphasis among 152 | these three; they all describe ‘‘behavior that is undefined’’. 153 | 154 | 这个意义下,**Constraints** 的规则和语法规则具有同等效力,尽管其中有一部分是形式语法说不清楚的(_constant-expression_) 。而剩下的,违反了就当未定义行为了。 155 | 156 | 在ISO C++中,这里的设计更加明确。 157 | 158 | **WG21/N4527** 159 | 160 | 161 | > ### 1.3.27 [defns.well.formed] 162 | > 163 | > **well-formed program**\ 164 | > C++ program constructed according to the syntax rules, diagnosable semantic rules, and the One Definition Rule (3.2).\ 165 | > 166 | > ## 1.4 Implementation compliance [intro.compliance] 167 | > 168 | > 1 The set of diagnosable rules consists of all syntactic and semantic rules in this International Standard except for those rules containing an explicit notation that “no diagnostic is required” or which are described as resulting in “undefined behavior.” 169 | 170 | 同逻辑学的习惯,“合式”被作为构造性正确的标准,是可以明确决定的——只不过仍然不要求实现必须确定。有别于未定义行为,这里明确区分了诊断要求,表示了更精细的区别。 171 | 172 | ODR 本质上来源于构造性语义规则,不过因为单独翻译链接模型和对于不同实体本身的复杂性,这里做了妥协,直接和语法规则、可诊断语义规则并列了。 173 | 174 | 另外值得一提的是,虽然 ISO C 和 ISO C++ 的 Annex A 都直接给出了类似的东西,但标题却不大一样:C 是 **Language syntax summary** ,C++ 是 **Grammar summary** 。 175 | 176 | 其实也不难理解:C++ 的语法和语义虽然在原则上多少是想要明确分离的,具体设计上却自己打脸。 177 | 178 | C 的语法接近上下文无关语法(context-free grammar) (考虑到抽风的指针语法所以不纯粹),而C++的情况明显糟糕得多。 179 | 180 | 因为语言要求的一些消歧义非形式文法规则,严格的、单纯的 C++ 语法分析器事实上并不存在。比如说声明中的 `()` 表示一个函数声明符还是初值符的一部分,不判断现有实体的类型是不知道的,而类型需要经过进一步上下文相关的语义分析才能确定。现有的实现基本上用的是有效然而无奈的笨办法:假定每种文法正确都试一遍,最后留下一个没错的就说明没歧义了。 181 | 182 | 换句话说,要说 C++ 的“语法”烂,正是因为它连“语法”是什么都说不清楚——规则的边界本来就是模糊的。 183 | 184 | 大概这个槽点,ISO C++ 的 Annex A 也就老实写 grammar 而不是 syntax 了,尽管和C一样,明明两者都没有给出形式语义…… 185 | 186 | Java 在这里就好得多,直接给出了文法定义,并明确使用上下文无关文法。 187 | 188 | (尽管实际上仍然只有形式语法……是 [Guy Steele](https://en.wikipedia.org/wiki/Guy_L._Steele,_Jr.) 参与起草的关系么?) 189 | 190 | **[JLS 8](https://docs.oracle.com/javase/specs/jls/se8/jls8.pdf)** 191 | 192 | > 2 Grammars 9\ 193 | > 2.1 Context-Free Grammars 9\ 194 | > 2.2 The Lexical Grammar 9\ 195 | > 2.3 The Syntactic Grammar 10\ 196 | > 2.4 Grammar Notation 10 197 | 198 | 当然,Java 的语义一锅乱七八糟的这里不展开了。 199 | 200 | C# 大体上抄的 Java ,以及C艹的部分糟烂(又是关于<>的疼货)…… 201 | 202 | **[ECMA-334 3 2005](https://www.ecma-international.org/publications/files/ECMA-ST/Ecma-334.pdf)** 203 | 204 | > 9.1 Programs\ 205 | > 9.2 Grammars\ 206 | > 9.2.1 Lexical grammar\ 207 | > 9.2.2 Syntactic grammar\ 208 | > 9.2.3 Grammar ambiguities 209 | 210 | ECMAScript 总体上也是一路货(否则干嘛叫 JavaScript ……) 211 | 212 | **[ECMA-262 5.1 2011](https://www.ecma-international.org/ecma-262/5.1/ECMA-262.pdf)** 213 | 214 | > 5 Notational Conventions\ 215 | > 5.1 Syntactic and Lexical Grammars\ 216 | > 5.1.1 Context-Free Grammars\ 217 | > 5.1.2 The Lexical and RegExp Grammars\ 218 | > 5.1.3 The Numeric String Grammar\ 219 | > 5.1.4 The Syntactic Grammar\ 220 | > 5.1.5 The JSON Grammar\ 221 | > 5.1.6 Grammar Notation 222 | 223 | C++/CLI 是直接照搬 ISO C++ 偷懒了。 224 | 225 | [Haskell 2010 Report](https://www.haskell.org/definition/haskell2010.pdf) 则只是提了下 notation ,看起来都没说啥 grammar ……不知道对混乱的设计有多少自知之明呢。 226 | 227 | 作为补充说明,[Scheme](https://en.wikipedia.org/wiki/Scheme_%28programming_language%29) 的 [RnRS](https://www.schemers.org/Documents/Standards/) 是少数给出了文法描述的形式语义的语言规范。正文符号太疼略。 228 | 229 | 顺便,注意 Scheme 的语法和 [S-expression](https://en.wikipedia.org/wiki/S-expression) 的语法是两回事。 230 | 231 | # 6.结语 232 | 233 | 抛开形式语法,没有一个实用语言的语法是足够简单到直接能一看就全记住的…… 234 | 235 | 那些提啥“只是语法”还有“语法书”的,真拿得出一本出来么。或者替我贴完 ISO C 的 Annex A 科普一下瞧瞧? 236 | 237 | (剩下的懒得写了自行脑补。) 238 | 239 | 附注:C++ 标准中一处和本文不符的提法[已被修正](https://github.com/cplusplus/draft/pull/790)。 240 | 241 | -------------------------------------------------------------------------------- /zh-CN/why-is-pointer-awful.md: -------------------------------------------------------------------------------- 1 | # [科普]为什么指针是个糟糕的语言特性 2 | 3 | Created @ 2015-08-23 07:27, rev2 2015-08-23 14:37, markdown @ 2015-09-16. 4 | 5 | # 0 引言 6 | 7 |   本文主要是写给自信知道“指针”是什么玩意儿的读者看的。最好看完再评论。(我大致上能确定 99% 以上的并没有足够重视这里的坑,不够了解现实。) 8 | 9 |   不知道指针是啥的虽然看了基本是浪费时间,不过至少务必记住:不是所有叫“指针”的东西都是一回事。 10 | 11 | # 1 什么是指针 12 | 13 |   本文所谓的指针(pointer) ,是指C和C++等语言中的内建的语言特性。 14 | 15 |   在不同范畴中指针这个概念有所不同。在体系结构规范中,指针指称特定的整数字节地址或者两个地址的差(地址偏移量),是整数数值;而在C和C++中,作为核心语言特性支持的指针是一类类型的统称。这两种完全不同的概念经常被混淆,造成一些稀里糊涂的问题(和数组混在一起的时候尤甚)。除非另行说明,本文总是指后者,并不对此进一步展开论述。 16 | 17 |   C和C++中,指针(右)值是具有指针类型的(右)值。指针值有时也会被和指针混淆,但在健全的理解下通常能消歧义,因此问题不大(数组也有类似的情况,但涉及转换,问题相对严重)。为清晰起见,在这里不会不加区分地使用。 18 | 19 |   注意,C++的成员指针(pointer-to-member) 明确地不是指针。尽管它的数值表示在一些情况下可能被实现为地址的偏移量,但语言中并不存在这种保证,实际也通常不那么简单。重复:成员指针不是这里讨论的指针。 20 | 21 |   此外,C++中,除了作为语言特性支持的内建(builtin) 指针,也有所谓的智能指针(smart pointer) 。后者在概念上也被 ISO C++11 以来的标准库正式支持。这里讨论的指针不包括这些智能指针,尽管后者和主题相关,并且会在下文重点讨论。 22 | 23 | # 2 什么是设计 24 | 25 |   这里讨论的设计,是指语言的设计,也就是语言规则的作者决定语言特性中应该存在什么和不存在什么的决策之下的抽象结果。 26 | 27 |   用户如何使用指针即语用问题是和本文主题相关的问题,会一并讨论,但和这里的设计是两个不同的话题。 28 | 29 | # 3 什么是糟糕 30 | 31 |   糟糕是一个形容词。 32 | 33 |   形容设计的糟糕从两个递进的视点得出:对照设计要解决的问题,即需求;对照同类解决方案,即语言中的其它特性或应用领域有交集的其它程序设计语言中的特性。 34 | 35 |   通俗地,糟糕以“不好用”和“并非不得不好用”来表现。 36 | 37 |   注意因为语言规则之间的相互作用,是否“好用”或者说要解决的问题,须结合使用场景下的其它问题一并讨论:一项特性即便能很好地解决某些问题,但若几乎总是引起其它难以回避的问题,那么至少是“不够好用”的;而要造成的问题麻烦到一定程度时就显然“不好用”了。 38 | 39 | # 4 指针有什么用 40 | 41 |   在说明不好用之前,首先需要了解有什么用。 42 | 43 |   这是一个发散的语用问题,但大多数用法都很浅显,清楚语言规则就并不难归纳。 44 | 45 | ## 4.1 指针和地址 46 | 47 |   C/C++ 的指针值和体系结构中的所说的指针(地址或地址偏移量)的基本作用类似,它用来指示数据存储的位置。 48 | 49 |   以体系结构的接口实现 C/C++ ,可以轻易保证相同类型的指针值到地址的映射是单射,即相同指针类型的指针值的不同的数值表示可以总是找到不同的地址对应,这样就可以在整数算术和关系操作的基础上毫无额外代价地定义指针算术和关系操作;而指针上的操作符 `*` 抽象的正是间接寻址操作。这就是一些用户口中的所谓“接近底层”。这种简单直接实现的最大好处就是容易以非常小的代价生成针对特定体系结构的代码。 50 | 51 |   一个需要注意的关键不同点在于,C/C++ 作为强类型语言(这里的用法也比较乱,指的是原本意义——默认具有静态类型检查),其中的值(value) 脱离类型讨论并没有意义,指针值也不例外。对象指针可以进行算术操作,但和整数地址算术的含义并不相同,这受到具体指针类型的影响——例如,`T*` 和整数的 `+` 操作和 `sizeof(T)` 相关;而函数指针并没有类似的意义。此外,需要不同间接操作层次的值如 `T*` 和 `T**` 也可被明确地静态地区分,光靠地址并不能做到这点。 52 | 53 |   然而,因为体系结构(硬件)实现的惯例,这个差异在往往能被利用(典型地,基址变址寻址),生成相对高效的代码。这是语言中保留指针算术的用途之一。另一方面,把地址相关的整数数值明确和一般的整数值区分,也明确了目的,使静态检查非预期的混用成为可能,有限地提供了类型安全性(例如,指针和指针不能相加)。 54 | 55 |   通过两个地址,或一个地址和表示字节大小的一个非负整数就可以标识出地址空间的区间范围。把地址替换为对象指针、字节大小替换为长度(指针值指向的连续元素的个数)同时限制取得指针的手段,能保留这种标记连续存储区域的功效,同时提供一定的可移植性。这种连续的存储在类型系统上被抽象为数组。不过应当注意,在可移植的要求下,实际上指针的语义依赖于数组。完全绕过数组的存在任意地构造一个指针值不能保证指向有效的对象或函数,进行间接操作基本上总会导致未定义行为。 56 | 57 |   另一个关键不同是空指针值(null pointer value) 并不保证有特定的地址对应,见下文。 58 | 59 | ## 4.2 存储资源管理 60 | 61 |   因为一个对象指针和长度可以用于表示连续的内存,而对象(排除 VLA )的大小能在翻译时静态确定,所以在已知大小的存储区域可以用一个对象指针值直接表示。 62 | 63 |   ISO C 标准库的 `malloc` 和 `calloc` 以及 ISO C++ 标准库的 `::operator new` 和 `::operator new[]` 等的返回值是典型的实例。 64 | 65 |   这里大小是由存储分配另外保存,这样释放时仍然只需要传递一个指针值即可。ISO C++14提供了 sized deallocation ,不过这并非强制。 66 | 67 | ## 4.3 参数传递 68 | 69 |   因为从分配函数中取得的指针表示的存储并不会如自动变量一样会被自动回收,同时指针有间接操作,而指针值作为对象类型的值可以作为参数传递,因此传递指向对象的指针值配合间接操作就可以模拟传递对象的引用。 70 | 71 | ## 4.4 基于存储的迭代 72 | 73 |   因为对象指针能表示存储位置,连续存储的布局由存储模型(以及基于数组的语义)规则限定,适当类型的指针值进行算术操作可以双向顺序迭代乃至随机访问连续存储的对象。 74 | 75 | ## 4.5 空指针值 76 | 77 |   指针类型是可空类型(nullable) 类型,约定特殊的空指针值表示不指向任何对象或函数,但可以进行有限的比较。 78 | 79 |   可空类型很容易用来表示可选(optional) 值:约定空指针表示值不存在,非空指针指向的对象或函数即存在的可选值。 80 | 81 |   空指针值还可以表示哨位(sentinal) 即迭代终止的标识。相对于具体存储区间结束的指针相比,空指针值是通用的,并不需要根据特定的区间使用不同的值。 82 | 83 |   注意空指针值的存储表示不一定是整数零值(这再次体现了人为预选的数值和地址的无关性),尽管使用零值一般能有更好的初始化性能。 84 | 85 | # 5 指针为什么不好用 86 | 87 |   既然标题已经确定了指针设计的糟糕,那么在“不好用”上自然有充分的理由。 88 | 89 |   总结就是,指针看上去能干很多事,但没一样事是完全干好的,还有的事(比如声明语法)甚至在一般意义上就特别差。 90 | 91 |   讽刺的是,第一个大规模使用这种指针的 C 语言作为UNIX系统的实现却完全违反了 UNIX 程序鼓吹的模块化设计哲学:只做一件事,并且把事做好。 92 | 93 |   为什么“程序”应当遵守的原则,分解到实现语言的特性的层次上,就可以罔顾设计原则乃至表现得相反了呢?难道这里不更应该体现接口的可组合性吗?耐人寻味。 94 | 95 | ## 5.1 使用的意图 96 | 97 |   首要的原罪就是指针能干太多事了,导致如果只需要其中的某些功能子集(几乎所有情况都是这样,实际上也不可能全用上,见下文)就不容易看清楚代码在做什么,也就是任何“正常”的使用与使用其它替代实现手段相比,都很容易明显损害代码的可读性。 98 | 99 |   要避免这点,要么放弃使用指针而使用其它替代,要么就不得不以文档(包括注释)等形式来把这些接口规格中大多不必出现的琐碎细节约定清楚。后者很容易显著增大实现和维护的工作量。 100 | 101 | ## 5.2 易错 102 | 103 |   因为意图不明的关系,使用指针的代码比使用其它更清晰的替代的代码更有机会错误,而指针本身的静态类型检查对此爱莫能助。 104 | 105 |   最显著和严重的错误可能是对于存储资源管理的错误。 106 | 107 |   注意C/C++语言要求去配函数的指针值参数若非空,则必须和适当的分配函数的返回(指针)值相等且不能以相同的值调用去配函数超过一次,否则程序行为未定义。 108 | 109 |   因为指针值并不保证翻译时确定,静态检查对此类误用效果很有限,要想安全使用且不泄露资源,用户必须清楚使用的指针是否可以被释放,然后准确保证从分配函数得到的非空指针值恰好作为参数调用正确的对应的去配函数一次——这里是否可以释放的所有权(ownership) 信息并没有编码在指针的类型之中。 110 | 111 |   注意,单看一个指针值,有或者没有所有权是确定的,不存在第三种状况。鉴于这两种状况互斥,因此一个指针值不可能同时是表示存在所有权的资源指针和表示不存在所有权的资源视图/观察者指针。这也就是上文说“不可能全用上”的原因。 112 | 113 |   然而事实是明确持有一个有所有权的指针,需要释放时,光看指针根本没法知道该使用哪个去配函数……更有甚者,其实光从指针上根本就看不出有没有所有权。 114 | 115 |   如果一个返回指针值的函数不幸没有文档描述清楚用户该如何处理资源释放问题,就面临了两难的风险:调用错误的去配函数或重复释放导致未定义行为,或者放置不管而至少产生泄漏的后果。 116 | 117 |   可能就是因为这样,WG14 ( C 标准委员会)有一条不成文的规矩:返回指针的函数总是不带所有权——也就是用户不应该释放这里的资源。 118 | 119 |   然而就连 Austin Group (起草 POSIX 标准的作者)对此都并不买账(更别提 GNU 等了),造成了接口设计上的冲突(详见 [WG14/N1174](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1174.pdf) ),可见这条默许的规则在 C 用户的范畴内整体上行不通。 120 | 121 |   用户该何去何从?看脸……(没有接口文档的自己踩坑怎么死?看着办。) 122 | 123 | ## 5.3 不必要的负担 124 | 125 |   这里最明显的例子是明明静态确定在不需要空指针的情况下不得不判断指针值是否为空,给程序运行带来不必要的开销。 126 | 127 |   [所谓的“空指针”滥觞于 C.A.R.Hoare 在 1960 年代的 ALGOL W 语言的发明。2009年,Hoare 在一个会议上为此道歉,原因是空指针特性引发了很多程序设计中的错误和漏洞。](https://en.wikipedia.org/wiki/Null_pointer#History) 128 | 129 |   盲目省略空指针值检查的导致使用指针间接操作的值引起未定行为的错误威力并不比上面资源管理的错误来得小。因此一旦接口沾染了指针,事情就复杂了——最容易的修复就是放弃使用指针这样的可空类型。 130 | 131 | ## 5.4 语法噪音 132 | 133 |   上面说的都是语义直接相关的语用困难。 134 | 135 |   事实上,即便不考虑语义问题,经验表明光是 C/C++ 的指针语法(严格来说不光是指针自己的问题,还有数组、C++ 的引用和 C++/CLI 的句柄等,都属于此类)也相当反直觉了。大部分用户遇到嵌套的指针声明符甚至都不能一下子看明白边界,更别说表示什么意思了。 136 | 137 |   对这个问题的主要变通是使用 `typedef` 。但未必每个接口都会老实用——比如 ISO C 的 `signal` 函数就没有用。所以遇到了用户还是要硬着头皮看。另外还可能有同时有使用 `typedef` 和不使用 `typedef` 名称并存然而两者等价的局面,此时用户就得当人肉编译器自行验证 `typedef` 和复杂声明符的等价性了…… 138 | 139 |   而现代的编译器也没能利用这样的语法带来简化。 140 | 141 |   鉴于这种看起来精巧实则无用的设计带来的困难,Bjarne Stroustrup 等在 C++ 尝试引入更直白的语法。但是,虽然 `trailing-return-type` syntax 是引入到 ISO C++11 里了,兼容 C 却不能排除旧的语法,结果就是对用户来说存在两套不完全兼容语法要学,编译器也得把两套语法都实现这样一个混乱局面…… 142 | 143 | ## 5.5 语义噪音 144 | 145 |   同样因为意图不明的关系,要让不同用法之间存在差异变得困难了。 146 | 147 |   举例来说,C++ 不需要内建指针模拟对象引用传递参数,所以看到 `->` 和一元操作 `*`(重载另说,但不抽风的重载不应该和这里的清晰性背道而驰)就可以大致确定此处进行的是非平凡(模拟参数传递)的操作。 148 | 149 |   考虑到模拟引用参数也必然不需要空指针值,这样一来差距更明显。 150 | 151 | ## 5.6 抽象的无能 152 | 153 |   或许抽象能力的缺失才是最大的现实问题,因为关乎高级语言的本质目的,而并非特定的个别需求。 154 | 155 |   一个例子是,迭代存储连续的序列用算术操作,为什么同样是迭代,链表就不能类似的语法呢? 156 | 157 |   不过只是“不好用”的角度并不容易集中体现这一点,此处先略过。 158 | 159 | ## 5.7 互操作性 160 | 161 |   和体系结构的交互或许是指针唯一合适的领域了。不过,这依赖于实现的假设,因此操作起来并不那么有普适性。 162 | 163 |   即便平时鼓吹“硬件友好”“接近底层”,事实上 C 就不存在对地址空间的抽象,还得靠厂商或者 WG14/N1169 这类几乎名不见经传的扩展。 164 | 165 |   倒是 C++ 标准库的分配器机制本来有要支持上面扩展的考虑不同的指针,虽然后来都流行平坦地址空间然后这个需求就死得差不多了…… 166 | 167 |   一个根本硬伤是,相同类型指针值到地址的映射是单射而不是满射——也就是任意一个地址即便在体系结构和实际机器的环境下允许,也有重重限制,根本不保证能用能自由操作的指针表示。 168 | 169 |   这样,关键时刻到底还得上体系结构相关的扩展乃至汇编和/或机器语言……(什么硬件友好接近底层,见鬼去吧(╯‵□′)╯︵┻━┻!) 170 | 171 | ## 5.8 理解的混乱 172 | 173 |   事实证明,指针自身的微妙规则以及和数组之间看似说不清道不明的关系给教材编写者以及初学者带来了极大麻烦。 174 | 175 |   不管是 Bjarne Stroustrup 鼓吹的 teachability 还是一般用户期待的“易用性”,指针的语法和语义规则都是重灾区。 176 | 177 |   总体来看,这种的问题的根源来源于指针这项语言特性自身的设计——包括是不是真的适合作为核心语言特性这点。 178 | 179 |   内建指针还制造了其它语言设计上的历史包袱: 180 | 181 | * C 语言引入的 `void*` 加剧了关于对象指针的混乱的理解。 182 | * 虽然 `void*` 本身并非不合理(比起普通的对象指针,去除了指针算术的更清晰的设计),但和指针不够撇清关系、使用指针语法一定程度上就是原罪。 183 | * 看上去这样设计的主要理由复用是 `void` 来节约关键词,和引入这个特性的目的没直接关系。不过“重载”关键词的含义本身就可能造成更多理解混乱(如 `static` ),算不上是好主意。 184 | * 大概这点问题比较明显,C++ 除了挪用 `static` 表示和具体类的对象无关的类成员外,之后倾向加新的关键词以及更保守的上下文相关的标识符(如 `final` )。 185 | * 即便是 ISO C ,也是使用新的关键词(比 C++ 直接占用用户可能使用的标识符更保守,使用带前缀 `_` 后跟大写字母的保留标识符)而不是复用已有的关键词来提供新特性。 186 | * GNU C 还有把 `void*` 当作类似 `char*` 来用的扩展,某种意义上是开倒车。 187 | * C 的 `void` 算是(总是不完整的)对象类型,看上去和这个设计之间看起来也有一定的关系。C++ 在对象类型中排除了 `void` ,不过仍然直接保留了 `void*` 。 188 | * C++ 的 `new` 表达式返回对象指针。从上面的分析看,这并不是合理的设计。 189 | * 返回的指针值是带有所有权的。直接遗忘返回的指针值而不释放就泄露了资源。以现在的观点看,应该返回的是带默认删除器的 `std::unique_ptr` 的实例,或者是单独的特设的智能指针。 190 | * 注意内建指针是内建特性并不是适合作为核心语言表达式结果的理由。以现在的观点看,完全可以像指定 `sizeof` 表达式的类型是 `std::size_t` 一样,指定 `new` 表达式求值的结果是标准库中的类型的实例。如 `nullptr` 的类型 `std::nullptr_t` 也是类似的(至于 `true` 、`false` 和 `nullptr` 这样的字面量,因为要作为右值又不能是枚举类型,才没有比直接提供新的关键字外的其它的更简单的选择)。 191 | * 只是当年并没有类模板来代替内建指针表达参数类型,不可能提供 `std::unique_ptr` 的实例,这只能是历史包袱了;但即便如 C 的 `_Atomic` 一般提供新的关键字来实现特设类型,演进的结果可能也好得多——当出来模板时,这样的关键字可以被改为模板。 192 | * 若如 `std::unique_ptr` 的实例这样的类型在当年就被支持,那么之后实际实现的 ABI 中可能就不会出现[不合理的歧视替代内建指针的这些类型的设计](https://itanium-cxx-abi.github.io/cxx-abi/abi.html#non-trivial),使现在的某些实现中许多类型仍然比内建指针在性能上受限而给使用内建指针提供借口。 193 | 194 | ## 5.9 谁来承担责任 195 | 196 |   可笑的是,缺陷这样明显的语言特性,一边在被各种集中地滥用和误用,一边被井底之蛙吹嘘为“ C 的灵魂”骗更多不知情者上当…… 197 | 198 |   容忍这样的缺陷和制造混乱代码的作者通常是同一拨用户。对于不良语用导致的后果却往往由合作的理解更透彻的维护者承担,把本可以满足更多现实需求的时间花在给脑残粉的烂代码擦屁股的破事上。 199 | 200 |   这是有多不公平呢? 201 | 202 | # 6 “指针”必须这样不好用吗/不用指针用啥 203 | 204 |   如果不限于内建指针,答案是否定的。 205 | 206 |   从指针几个有用和常用的使用惯例来看,搞清楚真实需求之后,很容易设计出更安全好用的机制。当然,得有足够的其它核心语言特性支持,类型系统羸弱的 C 只能靠边站。 207 | 208 |   对这里的缺陷修正得比较彻底而又比较流行的例子主要就是 C++ ,同时 C++ 也保留了指针的操作,反而更有必要澄清什么时候不适合用指针。所以以下以 C++ 为例(涉及的主要特性,其它现代语言,即便没有指针也大多有对应)。 209 | 210 | ## 6.1 了解意图、避免常见错误和提升可读性 211 | 212 |   若需要间接操作表示资源,使用带所有权的智能指针。同时可以自动管理资源,避免资源泄漏。不加封装地使用内建指针意味着更混乱的代码路径,通常是糟糕的代码。 213 | 214 |   若需要间接操作表示不带所有权的资源视图,使用不带所有权的特定智能指针类型,如 [WG21/N4282](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4282.pdf) 提议的 `observer_ptr` 来帮助表明意图。(这里使用内建指针的问题相对比较小,在没有其它选择的偷懒情况下,使用内建指针相对来说能够被容忍,因为带有所有权的指针已经被其它智能指针区分出去了。) 215 | 216 |   若需要传递引用,直接使用内建引用。在需要复制引用的场合,使用 `std::ref` 之类的包装。内建指针在此本质上毫无必要,并且无法使用大部分其它设施(只有 `std::bind` 等一些少数例外)。 217 | 218 |   若需要可空类型,使用 [WG21 N4480](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4480.html) 等规范的 `optional` 类型。(内建指针仍然是个可以忍耐的替代,但并不推荐。) 219 | 220 |   若需要迭代操作,使用迭代器(iterator) 。迭代器同时有更好的类型安全性、适应性和可扩展性。指针作为随机访问迭代器的特例是可以被使用的,但仍然应当小心行事。 221 | 222 |   通过划分典型应用场景,就基本解决了上面的最麻烦的一些问题。除了静态区分存在和不存在所有权相互矛盾之外,以上类型也是可以组合的,因此同时需要多种意图也不需要使用内建指针。 223 | 224 |   剩余**不得不**使用内建指针的情形,只有明确需要**互操作**的时候。这包括: 225 | 226 | * 和内建语言特性的互操作,主要是 `new` 表达式的求值结果。 227 | * 和 C 语言互操作,传递指针参数的函数参数和指针类型的函数返回值。 228 | * 其它依赖二进制接口的互操作,如 `asm` 语句中的操作。这依赖实现细节。 229 | 230 |   上述不带所有权的指针不是的必须使用的。鼓励不使用内建指针,有助于区分这样的情形和不得不使用内建指针的情形,可使接口的意图更清晰。这个方面 Bjarne Stroustrup 有[不同的意见](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1408r0.pdf),但除了使用智能指针会使一些代码更罗嗦(其实也就是拼写和多了 `<>` ,要绕过不能进行指针算术之类的问题而需要 `get()` 而变得罗嗦本来就是预料之中的)这个现实问题外,剩余的主张几乎**都**是站不住脚的: 231 | 232 | * 正因为 `T*` 的糟糕,才应该避免使用。 233 | * 使用 `T*` 的历史久远,说明误导产生的时间足够长,现在该是被改正的时候了。 234 | * 基于误会产生的错误的习惯并不会因为使用习惯而显得正确并消除其它问题。 235 | * 对一个名义类型系统(nominal type system) 主导类型检查的语言,接口上的显式类型可能会造成污染,但只要意图正确,原则上就不应该出现问题。 236 | * 作为无所有权指针的 `T*` 和 `observer_ptr` 的行为差异不应当被依赖。 237 | * 若替换 `T*` 为 `observer_ptr` 出现问题,正好说明原来的实现逻辑可能有问题,这样的 `T*` 并不被作为无所有权的指针使用而能被替换为 `observer_ptr` 。 238 | * 类似地,对 ABI 的不恰当依赖通常是应该避免的。 239 | * 代码演进的过程中变更类型产生的二进制不兼容并不是仅在替换指针类型时才会产生的问题,应当提前预备解决方案(如 inline namespace )。 240 | * 编译性能并不会是有意避免某些标准库类型的合理理由。 241 | * 退一步讲,至少 `std::unique_ptr` 等其它智能指针也有类似的问题。 242 | * 对特定的实现性能问题,如 `std::integer_sequence` ,必要时编译器可以直接提供内建支持。 243 | * 对代码大小导致的性能的问题也类似,例如 ABI 可以如标准库其它的类型一样可以提供缩短的符号,调试器可以为此提供特别支持,等等。 244 | * 不论 `T*` 表示不具有所有权的含义这种约定多普遍,它仍然有不可避免的混淆——特别是考虑内建一元操作符的语义规则中[把单一指针间接访问的目标作为一个元素的数组对象来考虑](https://eel.is/c++draft/expr.unary#op-3.2)的这样依赖指针算术的语义描述,以及 `new` 表达式。 245 | * 许多旧的代码根本无视这种约定。 246 | * 尽量避免使用 `T*` 才是真正的解决办法。注意使用 `observer_ptr` 作为 `T*` 的别名能解决其中的一些问题,但损失了类型检查,不容易发现误用,而不便清理没有遵守这种约定的旧的代码。 247 | * 反之,`T&` 则没有这种历史包袱,因此可放心使用——虽然 `&` 在声明符语法上也会有 `*` 类似的缺陷。 248 | * 即便推荐 `observer_ptr` 并非应是 WG21 的成熟的建议,如 [GSL](https://github.com/microsoft/gsl) 风格的建议也不应是替代选项。 249 | * 特别地,`owner` 这样的记法不但同样有罗嗦的问题,还无视对象保证自身资源可被释放(对象具有子对象的所有权)这个 C++ 隐含的默认规则;用 `owner` 代替 `T` 并不能像 `observer_ptr` 代替 `T*` 这般有消除意图不够明确的其它作用,多此一举。 250 | * [这里的一些讨论](https://www.reddit.com/r/cpp/comments/ak0vfg/)也类似地指出了其中的一些问题。 251 | 252 | ## 6.2 抽象能力和可扩展性 253 | 254 |   这集中体现在智能指针和迭代器与内建指针的对比之上。 255 | 256 |   内建指针的语义基本是被核心语言规则写死的,它并不能实现智能指针这样用户自定义资源所有权管理策略,以及迭代器这样的适配于不同实现构造的序列上。因为过于特殊,可以说是相当地无能。高下立判。 257 | 258 |   通过迭代器类别(iterator category) 的抽象层次和 [tag dispatching](https://www.boost.org/community/generic_programming.html#tag_dispatching) 这样基于重载(说穿了,一种模式匹配)的技巧,还能实现对不同性质的序列静态自动选取最优算法。不知比指针高了哪里去了。 259 | 260 |   当然,内建指针和典型体系结构实现之间的能力仍旧没有被取代。但指针在真正底层(比如说,地址空间)的抽象仍然一直是个坑。而且这明显不是高级语言的本职工作。如果不是照顾兼容性,让厂商实现成扩展并用标准库包装,说不定还不会像现在那么混乱。 261 | 262 | ## 6.3 约束更强的设计 263 | 264 |   (现代) C++ 是强烈强调静态类型存在感的语言。这种设计有利有弊,但从实践效果来看,正确地使用能够发挥静态类型检查的优势,是当代软件工程实践的重要趋势之一。(静态类型当然有非常鸡肋的地方然而现实是大部分用户根本连边都碰不到……注意缺乏元数据是 C++ 和标准化的锅,不是静态类型的锅。) 265 | 266 |   但是 C++ 限于历史包袱(兼容 C 、兼容现在各种代码),即便比 ISO C 敢于甩手扔包袱,也得考虑一下现实影响。在这个意义上,用户相对较少的小众语言以及新设计的语言就没有那么多顾虑,能将有目的的设计刻意发挥得更充分。 267 | 268 |   举两个稍微不怎么小众的例子。 269 | 270 |   一个是 [Haskell](https://www.haskell.org/) 。应该说重点不纯粹是静态类型的问题,而是在类型系统的设计上使用了对静态分析友好的较为系统化的设计。(而并不是像 C++ 那样一小坨一小坨地加特性,而这里最大坨的 Concept 被否了……) 271 | 272 |   当然这货主要用于开眼和拓展想象力,因为默认求值策略过于标新立异实际上不适合通用的需求,在 DSL 以上的实用还是算了。 273 | 274 |   另一个是 [Rust](https://www.rust-lang.org/) 。嗯,[设计的一个主要目标是取代 C/C++](https://www.infoq.com/news/2012/08/Interview-Rust) ,应该还算是比较现实(?)的。在这里值得一提的是有不少设计把上面的策略整合到核心语言特性上去了并且有系统的理论支撑,比如 [linear typing 是对 C++ 的 `std::unique_ptr` 强化](https://en.wikipedia.org/wiki/Substructural_type_system#Linear_type_systems) 。 275 | 276 |   姑且不论大杂烩的实用程度,这在科普上比较有意义。 277 | 278 | ## 6.4 复杂性谁来买单 279 | 280 |   有的用户可能会说,这么复杂,还是用内建指针直接偷懒算了。 281 | 282 |   对此我只能表示呵呵。你真有自觉到完全写清楚各个层次的接口文档表明语用?——注意,各个层次,包括现在当成实现细节而将来可能被接手的其他维护者当成内部接口使用的任意层次的“接口”。 283 | 284 |   如果: 285 | 286 |   (1)因为非自身原因只能用 C 这等无能玩意儿的而且真做得到及说服了其它倒腾这坨代码的(如果有)也同样做到上面所说的自觉,或者—— 287 | 288 |   (2)保证这坨代码不流入公众视野充实反面教材,同时实现者保证必要时时刻忏悔生产垃圾多出来的碳排放 289 | 290 |   那么当我没说。 291 | 292 |   否则……思想有多远就给我滚多远。 293 | 294 |   又不是叫你发明新语言特性自己实现编译器,都敢倒腾“底层”语言了,了解基本需求和解决方案这么点简单的份内之事都做不好还有脸生产垃圾污染环境让人擦屁股来添乱? 295 | 296 |   还是有谁逼你用这坨容易炸的东西了?(不懂适应现实?那么饿死活该。) 297 | 298 |   注意,业界从来不缺猪队友,少一头的确照样转(蠢代码照样蠢)。 299 | 300 | # 7 结语 301 | 302 |   略。 303 | 304 | --------------------------------------------------------------------------------