diff --git a/.github/workflows/build-examples.yml b/.github/workflows/build-examples.yml index 6572f3239..aece5eb50 100644 --- a/.github/workflows/build-examples.yml +++ b/.github/workflows/build-examples.yml @@ -27,10 +27,25 @@ jobs: - name: Install build tools run: | sudo apt-get update - sudo apt-get install -y cmake g++ ninja-build + sudo apt-get install -y cmake g++-14 ninja-build ccache + sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-14 100 + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-14 100 + sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/g++-14 100 + sudo update-alternatives --install /usr/bin/cc cc /usr/bin/gcc-14 100 + + - name: Cache ccache + uses: actions/cache@v4 + with: + path: ~/.cache/ccache + key: ccache-host-${{ github.run_id }} + restore-keys: | + ccache-host- - name: Build host examples - run: python3 scripts/build_examples.py --host + run: | + export CCACHE_DIR="$HOME/.cache/ccache" + mkdir -p "$CCACHE_DIR" + python3 scripts/build_examples.py --host build-stm32: name: STM32 Examples @@ -45,7 +60,18 @@ jobs: - name: Install build tools run: | sudo apt-get update - sudo apt-get install -y cmake gcc-arm-none-eabi libnewlib-arm-none-eabi ninja-build + sudo apt-get install -y cmake gcc-arm-none-eabi libnewlib-arm-none-eabi ninja-build ccache + + - name: Cache ccache + uses: actions/cache@v4 + with: + path: ~/.cache/ccache + key: ccache-stm32-${{ github.run_id }} + restore-keys: | + ccache-stm32- - name: Build STM32 examples - run: python3 scripts/build_examples.py --stm32 + run: | + export CCACHE_DIR="$HOME/.cache/ccache" + mkdir -p "$CCACHE_DIR" + python3 scripts/build_examples.py --stm32 diff --git a/.gitignore b/.gitignore index be5b59f1c..51dad3d59 100644 --- a/.gitignore +++ b/.gitignore @@ -7,21 +7,6 @@ build/ **/build/ *.build/ -# CMake -CMakeCache.txt -CMakeFiles/ -cmake_install.cmake -Makefile -*.cmake -!CMakeLists.txt - -# Compiled binaries -*.o -*.a -*.so -*.exe -*.out - # MkDocs build output site/ diff --git a/README.md b/README.md index 699ea24f0..431286be7 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ## 特色亮点 -- **系统化学习路径** -- 8 卷从入门到高级,每卷有明确前置知识,循序渐进 +- **系统化学习路径** -- 9 卷从入门到高级,每卷有明确前置知识,循序渐进 - **实战驱动** -- 每个概念配合可编译的 CMake 项目,而非孤立代码片段 - **多平台覆盖** -- STM32 / ESP32 / RP2040 嵌入式实战,不止于桌面端 - **标签导航** -- 按主题、C++ 标准、难度、平台等维度检索文章 @@ -32,6 +32,8 @@ graph LR V2 --> V3["卷三 标准库"] & V4["卷四 高级"] & V5["卷五 并发"] & V6["卷六 性能"] & V7["卷七 工程"] V2 --> V8["卷八 领域应用"] V8 --> E["嵌入式"] & N["网络"] & G["GUI"] & D["数据"] & A["算法"] + V2 --> V9["卷九 开源项目学习"] + V9 --> OC["Chrome 代码研读"] & OS["其他开源项目"] ``` ### 教程结构 @@ -46,6 +48,7 @@ graph LR | 六 | [性能优化](documents/vol6-performance/) -- CPU 缓存、SIMD、汇编阅读、基准测试 | 18-22 | advanced | 规划中 | | 七 | [软件工程实践](documents/vol7-engineering/) -- CMake、测试、静态分析、DevOps | 30-35 | intermediate | 规划中 | | 八 | [领域应用](documents/vol8-domains/) -- 嵌入式 / 网络 / GUI / 数据存储 / 算法 | 80-100 | intermediate | 编写中 | +| 九 | [开源项目学习](documents/vol9-open-source-project-learn/) -- 开源项目代码研读 | 13+ | intermediate | 编写中 | | - | [编译与链接深入](documents/compilation/) -- 预处理、汇编、链接、调试符号 | 10+ | intermediate | 已完成 | | - | [贯穿式实战项目](documents/projects/) -- 手写 STL、迷你 HTTP 服务器、嵌入式 OS | - | advanced | 规划中 | @@ -151,6 +154,7 @@ Tutorial_AwesomeModernCPP/ │ │ ├── gui-graphics/ # GUI 与图形 │ │ ├── data-storage/ # 数据存储 │ │ └── algorithms/ # 算法与数据结构 +│ ├── vol9-open-source-project-learn/ # 卷九:开源项目学习 │ ├── compilation/ # 编译与链接深入 │ ├── projects/ # 贯穿式实战项目 │ └── index.md # 教程首页 diff --git a/code/volumn_codes/vol9/chrome_design/CMakeLists.txt b/code/volumn_codes/vol9/chrome_design/CMakeLists.txt new file mode 100644 index 000000000..90b4a9072 --- /dev/null +++ b/code/volumn_codes/vol9/chrome_design/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.20) + +project(ChromeBase LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +include(cmake/CPM.cmake) + +message("Processing the OnceCallback") +add_subdirectory(once_callback) + +# Tests +enable_testing() +add_subdirectory(test) diff --git a/code/volumn_codes/vol9/chrome_design/cancel_token/cancel_token.hpp b/code/volumn_codes/vol9/chrome_design/cancel_token/cancel_token.hpp new file mode 100644 index 000000000..9308d432d --- /dev/null +++ b/code/volumn_codes/vol9/chrome_design/cancel_token/cancel_token.hpp @@ -0,0 +1,18 @@ +#pragma once +#include +#include + +namespace tamcpp::chrome { +class CancelableToken { + struct Flag { + std::atomic valid{true}; + }; + // All token should share a simple flags + std::shared_ptr flag_; + + public: + CancelableToken() : flag_(std::make_shared()) {} + void invalidate() { flag_->valid.store(false, std::memory_order_release); } + bool is_valid() const { return flag_->valid.load(std::memory_order_acquire); } +}; +} // namespace tamcpp::chrome diff --git a/code/volumn_codes/vol9/chrome_design/cmake/CPM.cmake b/code/volumn_codes/vol9/chrome_design/cmake/CPM.cmake new file mode 100644 index 000000000..832977c7e --- /dev/null +++ b/code/volumn_codes/vol9/chrome_design/cmake/CPM.cmake @@ -0,0 +1,1363 @@ +# CPM.cmake - CMake's missing package manager +# =========================================== +# See https://github.com/cpm-cmake/CPM.cmake for usage and update instructions. +# +# MIT License +# ----------- +#[[ + Copyright (c) 2019-2023 Lars Melchior and contributors + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +]] + +cmake_minimum_required(VERSION 3.14 FATAL_ERROR) + +# Initialize logging prefix +if(NOT CPM_INDENT) + set(CPM_INDENT + "CPM:" + CACHE INTERNAL "" + ) +endif() + +if(NOT COMMAND cpm_message) + function(cpm_message) + message(${ARGV}) + endfunction() +endif() + +if(DEFINED EXTRACTED_CPM_VERSION) + set(CURRENT_CPM_VERSION "${EXTRACTED_CPM_VERSION}${CPM_DEVELOPMENT}") +else() + set(CURRENT_CPM_VERSION 0.42.1) +endif() + +get_filename_component(CPM_CURRENT_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" REALPATH) +if(CPM_DIRECTORY) + if(NOT CPM_DIRECTORY STREQUAL CPM_CURRENT_DIRECTORY) + if(CPM_VERSION VERSION_LESS CURRENT_CPM_VERSION) + message( + AUTHOR_WARNING + "${CPM_INDENT} \ +A dependency is using a more recent CPM version (${CURRENT_CPM_VERSION}) than the current project (${CPM_VERSION}). \ +It is recommended to upgrade CPM to the most recent version. \ +See https://github.com/cpm-cmake/CPM.cmake for more information." + ) + endif() + if(${CMAKE_VERSION} VERSION_LESS "3.17.0") + include(FetchContent) + endif() + return() + endif() + + get_property( + CPM_INITIALIZED GLOBAL "" + PROPERTY CPM_INITIALIZED + SET + ) + if(CPM_INITIALIZED) + return() + endif() +endif() + +if(CURRENT_CPM_VERSION MATCHES "development-version") + message( + WARNING "${CPM_INDENT} Your project is using an unstable development version of CPM.cmake. \ +Please update to a recent release if possible. \ +See https://github.com/cpm-cmake/CPM.cmake for details." + ) +endif() + +set_property(GLOBAL PROPERTY CPM_INITIALIZED true) + +macro(cpm_set_policies) + # the policy allows us to change options without caching + cmake_policy(SET CMP0077 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) + + # the policy allows us to change set(CACHE) without caching + if(POLICY CMP0126) + cmake_policy(SET CMP0126 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0126 NEW) + endif() + + # The policy uses the download time for timestamp, instead of the timestamp in the archive. This + # allows for proper rebuilds when a projects url changes + if(POLICY CMP0135) + cmake_policy(SET CMP0135 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0135 NEW) + endif() + + # treat relative git repository paths as being relative to the parent project's remote + if(POLICY CMP0150) + cmake_policy(SET CMP0150 NEW) + set(CMAKE_POLICY_DEFAULT_CMP0150 NEW) + endif() +endmacro() +cpm_set_policies() + +option(CPM_USE_LOCAL_PACKAGES "Always try to use `find_package` to get dependencies" + $ENV{CPM_USE_LOCAL_PACKAGES} +) +option(CPM_LOCAL_PACKAGES_ONLY "Only use `find_package` to get dependencies" + $ENV{CPM_LOCAL_PACKAGES_ONLY} +) +option(CPM_DOWNLOAD_ALL "Always download dependencies from source" $ENV{CPM_DOWNLOAD_ALL}) +option(CPM_DONT_UPDATE_MODULE_PATH "Don't update the module path to allow using find_package" + $ENV{CPM_DONT_UPDATE_MODULE_PATH} +) +option(CPM_DONT_CREATE_PACKAGE_LOCK "Don't create a package lock file in the binary path" + $ENV{CPM_DONT_CREATE_PACKAGE_LOCK} +) +option(CPM_INCLUDE_ALL_IN_PACKAGE_LOCK + "Add all packages added through CPM.cmake to the package lock" + $ENV{CPM_INCLUDE_ALL_IN_PACKAGE_LOCK} +) +option(CPM_USE_NAMED_CACHE_DIRECTORIES + "Use additional directory of package name in cache on the most nested level." + $ENV{CPM_USE_NAMED_CACHE_DIRECTORIES} +) + +set(CPM_VERSION + ${CURRENT_CPM_VERSION} + CACHE INTERNAL "" +) +set(CPM_DIRECTORY + ${CPM_CURRENT_DIRECTORY} + CACHE INTERNAL "" +) +set(CPM_FILE + ${CMAKE_CURRENT_LIST_FILE} + CACHE INTERNAL "" +) +set(CPM_PACKAGES + "" + CACHE INTERNAL "" +) +set(CPM_DRY_RUN + OFF + CACHE INTERNAL "Don't download or configure dependencies (for testing)" +) + +if(DEFINED ENV{CPM_SOURCE_CACHE}) + set(CPM_SOURCE_CACHE_DEFAULT $ENV{CPM_SOURCE_CACHE}) +else() + set(CPM_SOURCE_CACHE_DEFAULT OFF) +endif() + +set(CPM_SOURCE_CACHE + ${CPM_SOURCE_CACHE_DEFAULT} + CACHE PATH "Directory to download CPM dependencies" +) + +if(NOT CPM_DONT_UPDATE_MODULE_PATH AND NOT DEFINED CMAKE_FIND_PACKAGE_REDIRECTS_DIR) + set(CPM_MODULE_PATH + "${CMAKE_BINARY_DIR}/CPM_modules" + CACHE INTERNAL "" + ) + # remove old modules + file(REMOVE_RECURSE ${CPM_MODULE_PATH}) + file(MAKE_DIRECTORY ${CPM_MODULE_PATH}) + # locally added CPM modules should override global packages + set(CMAKE_MODULE_PATH "${CPM_MODULE_PATH};${CMAKE_MODULE_PATH}") +endif() + +if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + set(CPM_PACKAGE_LOCK_FILE + "${CMAKE_BINARY_DIR}/cpm-package-lock.cmake" + CACHE INTERNAL "" + ) + file(WRITE ${CPM_PACKAGE_LOCK_FILE} + "# CPM Package Lock\n# This file should be committed to version control\n\n" + ) +endif() + +include(FetchContent) + +# Try to infer package name from git repository uri (path or url) +function(cpm_package_name_from_git_uri URI RESULT) + if("${URI}" MATCHES "([^/:]+)/?.git/?$") + set(${RESULT} + ${CMAKE_MATCH_1} + PARENT_SCOPE + ) + else() + unset(${RESULT} PARENT_SCOPE) + endif() +endfunction() + +# Find the shortest hash that can be used eg, if origin_hash is +# cccb77ae9609d2768ed80dd42cec54f77b1f1455 the following files will be checked, until one is found +# that is either empty (allowing us to assign origin_hash), or whose contents matches ${origin_hash} +# +# * .../cccb.hash +# * .../cccb77ae.hash +# * .../cccb77ae9609.hash +# * .../cccb77ae9609d276.hash +# * etc +# +# We will be able to use a shorter path with very high probability, but in the (rare) event that the +# first couple characters collide, we will check longer and longer substrings. +function(cpm_get_shortest_hash source_cache_dir origin_hash short_hash_output_var) + # for compatibility with caches populated by a previous version of CPM, check if a directory using + # the full hash already exists + if(EXISTS "${source_cache_dir}/${origin_hash}") + set(${short_hash_output_var} + "${origin_hash}" + PARENT_SCOPE + ) + return() + endif() + + foreach(len RANGE 4 40 4) + string(SUBSTRING "${origin_hash}" 0 ${len} short_hash) + set(hash_lock ${source_cache_dir}/${short_hash}.lock) + set(hash_fp ${source_cache_dir}/${short_hash}.hash) + # Take a lock, so we don't have a race condition with another instance of cmake. We will release + # this lock when we can, however, if there is an error, we want to ensure it gets released on + # it's own on exit from the function. + file(LOCK ${hash_lock} GUARD FUNCTION) + + # Load the contents of .../${short_hash}.hash + file(TOUCH ${hash_fp}) + file(READ ${hash_fp} hash_fp_contents) + + if(hash_fp_contents STREQUAL "") + # Write the origin hash + file(WRITE ${hash_fp} ${origin_hash}) + file(LOCK ${hash_lock} RELEASE) + break() + elseif(hash_fp_contents STREQUAL origin_hash) + file(LOCK ${hash_lock} RELEASE) + break() + else() + file(LOCK ${hash_lock} RELEASE) + endif() + endforeach() + set(${short_hash_output_var} + "${short_hash}" + PARENT_SCOPE + ) +endfunction() + +# Try to infer package name and version from a url +function(cpm_package_name_and_ver_from_url url outName outVer) + if(url MATCHES "[/\\?]([a-zA-Z0-9_\\.-]+)\\.(tar|tar\\.gz|tar\\.bz2|zip|ZIP)(\\?|/|$)") + # We matched an archive + set(filename "${CMAKE_MATCH_1}") + + if(filename MATCHES "([a-zA-Z0-9_\\.-]+)[_-]v?(([0-9]+\\.)*[0-9]+[a-zA-Z0-9]*)") + # We matched - (ie foo-1.2.3) + set(${outName} + "${CMAKE_MATCH_1}" + PARENT_SCOPE + ) + set(${outVer} + "${CMAKE_MATCH_2}" + PARENT_SCOPE + ) + elseif(filename MATCHES "(([0-9]+\\.)+[0-9]+[a-zA-Z0-9]*)") + # We couldn't find a name, but we found a version + # + # In many cases (which we don't handle here) the url would look something like + # `irrelevant/ACTUAL_PACKAGE_NAME/irrelevant/1.2.3.zip`. In such a case we can't possibly + # distinguish the package name from the irrelevant bits. Moreover if we try to match the + # package name from the filename, we'd get bogus at best. + unset(${outName} PARENT_SCOPE) + set(${outVer} + "${CMAKE_MATCH_1}" + PARENT_SCOPE + ) + else() + # Boldly assume that the file name is the package name. + # + # Yes, something like `irrelevant/ACTUAL_NAME/irrelevant/download.zip` will ruin our day, but + # such cases should be quite rare. No popular service does this... we think. + set(${outName} + "${filename}" + PARENT_SCOPE + ) + unset(${outVer} PARENT_SCOPE) + endif() + else() + # No ideas yet what to do with non-archives + unset(${outName} PARENT_SCOPE) + unset(${outVer} PARENT_SCOPE) + endif() +endfunction() + +function(cpm_find_package NAME VERSION) + string(REPLACE " " ";" EXTRA_ARGS "${ARGN}") + find_package(${NAME} ${VERSION} ${EXTRA_ARGS} QUIET) + if(${CPM_ARGS_NAME}_FOUND) + if(DEFINED ${CPM_ARGS_NAME}_VERSION) + set(VERSION ${${CPM_ARGS_NAME}_VERSION}) + endif() + cpm_message(STATUS "${CPM_INDENT} Using local package ${CPM_ARGS_NAME}@${VERSION}") + CPMRegisterPackage(${CPM_ARGS_NAME} "${VERSION}") + set(CPM_PACKAGE_FOUND + YES + PARENT_SCOPE + ) + else() + set(CPM_PACKAGE_FOUND + NO + PARENT_SCOPE + ) + endif() +endfunction() + +# Create a custom FindXXX.cmake module for a CPM package This prevents `find_package(NAME)` from +# finding the system library +function(cpm_create_module_file Name) + if(NOT CPM_DONT_UPDATE_MODULE_PATH) + if(DEFINED CMAKE_FIND_PACKAGE_REDIRECTS_DIR) + # Redirect find_package calls to the CPM package. This is what FetchContent does when you set + # OVERRIDE_FIND_PACKAGE. The CMAKE_FIND_PACKAGE_REDIRECTS_DIR works for find_package in CONFIG + # mode, unlike the Find${Name}.cmake fallback. CMAKE_FIND_PACKAGE_REDIRECTS_DIR is not defined + # in script mode, or in CMake < 3.24. + # https://cmake.org/cmake/help/latest/module/FetchContent.html#fetchcontent-find-package-integration-examples + string(TOLOWER ${Name} NameLower) + file(WRITE ${CMAKE_FIND_PACKAGE_REDIRECTS_DIR}/${NameLower}-config.cmake + "include(\"\${CMAKE_CURRENT_LIST_DIR}/${NameLower}-extra.cmake\" OPTIONAL)\n" + "include(\"\${CMAKE_CURRENT_LIST_DIR}/${Name}Extra.cmake\" OPTIONAL)\n" + ) + file(WRITE ${CMAKE_FIND_PACKAGE_REDIRECTS_DIR}/${NameLower}-config-version.cmake + "set(PACKAGE_VERSION_COMPATIBLE TRUE)\n" "set(PACKAGE_VERSION_EXACT TRUE)\n" + ) + else() + file(WRITE ${CPM_MODULE_PATH}/Find${Name}.cmake + "include(\"${CPM_FILE}\")\n${ARGN}\nset(${Name}_FOUND TRUE)" + ) + endif() + endif() +endfunction() + +# Find a package locally or fallback to CPMAddPackage +function(CPMFindPackage) + set(oneValueArgs NAME VERSION GIT_TAG FIND_PACKAGE_ARGUMENTS) + + cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "" ${ARGN}) + + if(NOT DEFINED CPM_ARGS_VERSION) + if(DEFINED CPM_ARGS_GIT_TAG) + cpm_get_version_from_git_tag("${CPM_ARGS_GIT_TAG}" CPM_ARGS_VERSION) + endif() + endif() + + set(downloadPackage ${CPM_DOWNLOAD_ALL}) + if(DEFINED CPM_DOWNLOAD_${CPM_ARGS_NAME}) + set(downloadPackage ${CPM_DOWNLOAD_${CPM_ARGS_NAME}}) + elseif(DEFINED ENV{CPM_DOWNLOAD_${CPM_ARGS_NAME}}) + set(downloadPackage $ENV{CPM_DOWNLOAD_${CPM_ARGS_NAME}}) + endif() + if(downloadPackage) + CPMAddPackage(${ARGN}) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + cpm_find_package(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" ${CPM_ARGS_FIND_PACKAGE_ARGUMENTS}) + + if(NOT CPM_PACKAGE_FOUND) + CPMAddPackage(${ARGN}) + cpm_export_variables(${CPM_ARGS_NAME}) + endif() + +endfunction() + +# checks if a package has been added before +function(cpm_check_if_package_already_added CPM_ARGS_NAME CPM_ARGS_VERSION) + if("${CPM_ARGS_NAME}" IN_LIST CPM_PACKAGES) + CPMGetPackageVersion(${CPM_ARGS_NAME} CPM_PACKAGE_VERSION) + if("${CPM_PACKAGE_VERSION}" VERSION_LESS "${CPM_ARGS_VERSION}") + message( + WARNING + "${CPM_INDENT} Requires a newer version of ${CPM_ARGS_NAME} (${CPM_ARGS_VERSION}) than currently included (${CPM_PACKAGE_VERSION})." + ) + endif() + cpm_get_fetch_properties(${CPM_ARGS_NAME}) + set(${CPM_ARGS_NAME}_ADDED NO) + set(CPM_PACKAGE_ALREADY_ADDED + YES + PARENT_SCOPE + ) + cpm_export_variables(${CPM_ARGS_NAME}) + else() + set(CPM_PACKAGE_ALREADY_ADDED + NO + PARENT_SCOPE + ) + endif() +endfunction() + +# Parse the argument of CPMAddPackage in case a single one was provided and convert it to a list of +# arguments which can then be parsed idiomatically. For example gh:foo/bar@1.2.3 will be converted +# to: GITHUB_REPOSITORY;foo/bar;VERSION;1.2.3 +function(cpm_parse_add_package_single_arg arg outArgs) + # Look for a scheme + if("${arg}" MATCHES "^([a-zA-Z]+):(.+)$") + string(TOLOWER "${CMAKE_MATCH_1}" scheme) + set(uri "${CMAKE_MATCH_2}") + + # Check for CPM-specific schemes + if(scheme STREQUAL "gh") + set(out "GITHUB_REPOSITORY;${uri}") + set(packageType "git") + elseif(scheme STREQUAL "gl") + set(out "GITLAB_REPOSITORY;${uri}") + set(packageType "git") + elseif(scheme STREQUAL "bb") + set(out "BITBUCKET_REPOSITORY;${uri}") + set(packageType "git") + # A CPM-specific scheme was not found. Looks like this is a generic URL so try to determine + # type + elseif(arg MATCHES ".git/?(@|#|$)") + set(out "GIT_REPOSITORY;${arg}") + set(packageType "git") + else() + # Fall back to a URL + set(out "URL;${arg}") + set(packageType "archive") + + # We could also check for SVN since FetchContent supports it, but SVN is so rare these days. + # We just won't bother with the additional complexity it will induce in this function. SVN is + # done by multi-arg + endif() + else() + if(arg MATCHES ".git/?(@|#|$)") + set(out "GIT_REPOSITORY;${arg}") + set(packageType "git") + else() + # Give up + message(FATAL_ERROR "${CPM_INDENT} Can't determine package type of '${arg}'") + endif() + endif() + + # For all packages we interpret @... as version. Only replace the last occurrence. Thus URIs + # containing '@' can be used + string(REGEX REPLACE "@([^@]+)$" ";VERSION;\\1" out "${out}") + + # Parse the rest according to package type + if(packageType STREQUAL "git") + # For git repos we interpret #... as a tag or branch or commit hash + string(REGEX REPLACE "#([^#]+)$" ";GIT_TAG;\\1" out "${out}") + elseif(packageType STREQUAL "archive") + # For archives we interpret #... as a URL hash. + string(REGEX REPLACE "#([^#]+)$" ";URL_HASH;\\1" out "${out}") + # We don't try to parse the version if it's not provided explicitly. cpm_get_version_from_url + # should do this at a later point + else() + # We should never get here. This is an assertion and hitting it means there's a problem with the + # code above. A packageType was set, but not handled by this if-else. + message(FATAL_ERROR "${CPM_INDENT} Unsupported package type '${packageType}' of '${arg}'") + endif() + + set(${outArgs} + ${out} + PARENT_SCOPE + ) +endfunction() + +# Check that the working directory for a git repo is clean +function(cpm_check_git_working_dir_is_clean repoPath gitTag isClean) + + find_package(Git REQUIRED) + + if(NOT GIT_EXECUTABLE) + # No git executable, assume directory is clean + set(${isClean} + TRUE + PARENT_SCOPE + ) + return() + endif() + + # check for uncommitted changes + execute_process( + COMMAND ${GIT_EXECUTABLE} status --porcelain + RESULT_VARIABLE resultGitStatus + OUTPUT_VARIABLE repoStatus + OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET + WORKING_DIRECTORY ${repoPath} + ) + if(resultGitStatus) + # not supposed to happen, assume clean anyway + message(WARNING "${CPM_INDENT} Calling git status on folder ${repoPath} failed") + set(${isClean} + TRUE + PARENT_SCOPE + ) + return() + endif() + + if(NOT "${repoStatus}" STREQUAL "") + set(${isClean} + FALSE + PARENT_SCOPE + ) + return() + endif() + + # check for committed changes + execute_process( + COMMAND ${GIT_EXECUTABLE} diff -s --exit-code ${gitTag} + RESULT_VARIABLE resultGitDiff + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_QUIET + WORKING_DIRECTORY ${repoPath} + ) + + if(${resultGitDiff} EQUAL 0) + set(${isClean} + TRUE + PARENT_SCOPE + ) + else() + set(${isClean} + FALSE + PARENT_SCOPE + ) + endif() + +endfunction() + +# Add PATCH_COMMAND to CPM_ARGS_UNPARSED_ARGUMENTS. This method consumes a list of files in ARGN +# then generates a `PATCH_COMMAND` appropriate for `ExternalProject_Add()`. This command is appended +# to the parent scope's `CPM_ARGS_UNPARSED_ARGUMENTS`. +function(cpm_add_patches) + # Return if no patch files are supplied. + if(NOT ARGN) + return() + endif() + + # Find the patch program. + find_program(PATCH_EXECUTABLE patch) + if(CMAKE_HOST_WIN32 AND NOT PATCH_EXECUTABLE) + # The Windows git executable is distributed with patch.exe. Find the path to the executable, if + # it exists, then search `../usr/bin` and `../../usr/bin` for patch.exe. + find_package(Git QUIET) + if(GIT_EXECUTABLE) + get_filename_component(extra_search_path ${GIT_EXECUTABLE} DIRECTORY) + get_filename_component(extra_search_path_1up ${extra_search_path} DIRECTORY) + get_filename_component(extra_search_path_2up ${extra_search_path_1up} DIRECTORY) + find_program( + PATCH_EXECUTABLE patch HINTS "${extra_search_path_1up}/usr/bin" + "${extra_search_path_2up}/usr/bin" + ) + endif() + endif() + if(NOT PATCH_EXECUTABLE) + message(FATAL_ERROR "Couldn't find `patch` executable to use with PATCHES keyword.") + endif() + + # Create a temporary + set(temp_list ${CPM_ARGS_UNPARSED_ARGUMENTS}) + + # Ensure each file exists (or error out) and add it to the list. + set(first_item True) + foreach(PATCH_FILE ${ARGN}) + # Make sure the patch file exists, if we can't find it, try again in the current directory. + if(NOT EXISTS "${PATCH_FILE}") + if(NOT EXISTS "${CMAKE_CURRENT_LIST_DIR}/${PATCH_FILE}") + message(FATAL_ERROR "Couldn't find patch file: '${PATCH_FILE}'") + endif() + set(PATCH_FILE "${CMAKE_CURRENT_LIST_DIR}/${PATCH_FILE}") + endif() + + # Convert to absolute path for use with patch file command. + get_filename_component(PATCH_FILE "${PATCH_FILE}" ABSOLUTE) + + # The first patch entry must be preceded by "PATCH_COMMAND" while the following items are + # preceded by "&&". + if(first_item) + set(first_item False) + list(APPEND temp_list "PATCH_COMMAND") + else() + list(APPEND temp_list "&&") + endif() + # Add the patch command to the list + list(APPEND temp_list "${PATCH_EXECUTABLE}" "-p1" "<" "${PATCH_FILE}") + endforeach() + + # Move temp out into parent scope. + set(CPM_ARGS_UNPARSED_ARGUMENTS + ${temp_list} + PARENT_SCOPE + ) + +endfunction() + +# method to overwrite internal FetchContent properties, to allow using CPM.cmake to overload +# FetchContent calls. As these are internal cmake properties, this method should be used carefully +# and may need modification in future CMake versions. Source: +# https://github.com/Kitware/CMake/blob/dc3d0b5a0a7d26d43d6cfeb511e224533b5d188f/Modules/FetchContent.cmake#L1152 +function(cpm_override_fetchcontent contentName) + cmake_parse_arguments(PARSE_ARGV 1 arg "" "SOURCE_DIR;BINARY_DIR" "") + if(NOT "${arg_UNPARSED_ARGUMENTS}" STREQUAL "") + message(FATAL_ERROR "${CPM_INDENT} Unsupported arguments: ${arg_UNPARSED_ARGUMENTS}") + endif() + + string(TOLOWER ${contentName} contentNameLower) + set(prefix "_FetchContent_${contentNameLower}") + + set(propertyName "${prefix}_sourceDir") + define_property( + GLOBAL + PROPERTY ${propertyName} + BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" + FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" + ) + set_property(GLOBAL PROPERTY ${propertyName} "${arg_SOURCE_DIR}") + + set(propertyName "${prefix}_binaryDir") + define_property( + GLOBAL + PROPERTY ${propertyName} + BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" + FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" + ) + set_property(GLOBAL PROPERTY ${propertyName} "${arg_BINARY_DIR}") + + set(propertyName "${prefix}_populated") + define_property( + GLOBAL + PROPERTY ${propertyName} + BRIEF_DOCS "Internal implementation detail of FetchContent_Populate()" + FULL_DOCS "Details used by FetchContent_Populate() for ${contentName}" + ) + set_property(GLOBAL PROPERTY ${propertyName} TRUE) +endfunction() + +# Download and add a package from source +function(CPMAddPackage) + cpm_set_policies() + + set(oneValueArgs + NAME + FORCE + VERSION + GIT_TAG + DOWNLOAD_ONLY + GITHUB_REPOSITORY + GITLAB_REPOSITORY + BITBUCKET_REPOSITORY + GIT_REPOSITORY + SOURCE_DIR + FIND_PACKAGE_ARGUMENTS + NO_CACHE + SYSTEM + GIT_SHALLOW + EXCLUDE_FROM_ALL + SOURCE_SUBDIR + CUSTOM_CACHE_KEY + ) + + set(multiValueArgs URL OPTIONS DOWNLOAD_COMMAND PATCHES) + + list(LENGTH ARGN argnLength) + + # Parse single shorthand argument + if(argnLength EQUAL 1) + cpm_parse_add_package_single_arg("${ARGN}" ARGN) + + # The shorthand syntax implies EXCLUDE_FROM_ALL and SYSTEM + set(ARGN "${ARGN};EXCLUDE_FROM_ALL;YES;SYSTEM;YES;") + + # Parse URI shorthand argument + elseif(argnLength GREATER 1 AND "${ARGV0}" STREQUAL "URI") + list(REMOVE_AT ARGN 0 1) # remove "URI gh:<...>@version#tag" + cpm_parse_add_package_single_arg("${ARGV1}" ARGV0) + + set(ARGN "${ARGV0};EXCLUDE_FROM_ALL;YES;SYSTEM;YES;${ARGN}") + endif() + + cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "${multiValueArgs}" "${ARGN}") + + # Set default values for arguments + if(NOT DEFINED CPM_ARGS_VERSION) + if(DEFINED CPM_ARGS_GIT_TAG) + cpm_get_version_from_git_tag("${CPM_ARGS_GIT_TAG}" CPM_ARGS_VERSION) + endif() + endif() + + if(CPM_ARGS_DOWNLOAD_ONLY) + set(DOWNLOAD_ONLY ${CPM_ARGS_DOWNLOAD_ONLY}) + else() + set(DOWNLOAD_ONLY NO) + endif() + + if(DEFINED CPM_ARGS_GITHUB_REPOSITORY) + set(CPM_ARGS_GIT_REPOSITORY "https://github.com/${CPM_ARGS_GITHUB_REPOSITORY}.git") + elseif(DEFINED CPM_ARGS_GITLAB_REPOSITORY) + set(CPM_ARGS_GIT_REPOSITORY "https://gitlab.com/${CPM_ARGS_GITLAB_REPOSITORY}.git") + elseif(DEFINED CPM_ARGS_BITBUCKET_REPOSITORY) + set(CPM_ARGS_GIT_REPOSITORY "https://bitbucket.org/${CPM_ARGS_BITBUCKET_REPOSITORY}.git") + endif() + + if(DEFINED CPM_ARGS_GIT_REPOSITORY) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_REPOSITORY ${CPM_ARGS_GIT_REPOSITORY}) + if(NOT DEFINED CPM_ARGS_GIT_TAG) + set(CPM_ARGS_GIT_TAG v${CPM_ARGS_VERSION}) + endif() + + # If a name wasn't provided, try to infer it from the git repo + if(NOT DEFINED CPM_ARGS_NAME) + cpm_package_name_from_git_uri(${CPM_ARGS_GIT_REPOSITORY} CPM_ARGS_NAME) + endif() + endif() + + set(CPM_SKIP_FETCH FALSE) + + if(DEFINED CPM_ARGS_GIT_TAG) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_TAG ${CPM_ARGS_GIT_TAG}) + # If GIT_SHALLOW is explicitly specified, honor the value. + if(DEFINED CPM_ARGS_GIT_SHALLOW) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_SHALLOW ${CPM_ARGS_GIT_SHALLOW}) + endif() + endif() + + if(DEFINED CPM_ARGS_URL) + # If a name or version aren't provided, try to infer them from the URL + list(GET CPM_ARGS_URL 0 firstUrl) + cpm_package_name_and_ver_from_url(${firstUrl} nameFromUrl verFromUrl) + # If we fail to obtain name and version from the first URL, we could try other URLs if any. + # However multiple URLs are expected to be quite rare, so for now we won't bother. + + # If the caller provided their own name and version, they trump the inferred ones. + if(NOT DEFINED CPM_ARGS_NAME) + set(CPM_ARGS_NAME ${nameFromUrl}) + endif() + if(NOT DEFINED CPM_ARGS_VERSION) + set(CPM_ARGS_VERSION ${verFromUrl}) + endif() + + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS URL "${CPM_ARGS_URL}") + endif() + + # Check for required arguments + + if(NOT DEFINED CPM_ARGS_NAME) + message( + FATAL_ERROR + "${CPM_INDENT} 'NAME' was not provided and couldn't be automatically inferred for package added with arguments: '${ARGN}'" + ) + endif() + + # Check if package has been added before + cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}") + if(CPM_PACKAGE_ALREADY_ADDED) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + # Check for manual overrides + if(NOT CPM_ARGS_FORCE AND NOT "${CPM_${CPM_ARGS_NAME}_SOURCE}" STREQUAL "") + set(PACKAGE_SOURCE ${CPM_${CPM_ARGS_NAME}_SOURCE}) + set(CPM_${CPM_ARGS_NAME}_SOURCE "") + CPMAddPackage( + NAME "${CPM_ARGS_NAME}" + SOURCE_DIR "${PACKAGE_SOURCE}" + EXCLUDE_FROM_ALL "${CPM_ARGS_EXCLUDE_FROM_ALL}" + SYSTEM "${CPM_ARGS_SYSTEM}" + PATCHES "${CPM_ARGS_PATCHES}" + OPTIONS "${CPM_ARGS_OPTIONS}" + SOURCE_SUBDIR "${CPM_ARGS_SOURCE_SUBDIR}" + DOWNLOAD_ONLY "${DOWNLOAD_ONLY}" + FORCE True + ) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + # Check for available declaration + if(NOT CPM_ARGS_FORCE AND NOT "${CPM_DECLARATION_${CPM_ARGS_NAME}}" STREQUAL "") + set(declaration ${CPM_DECLARATION_${CPM_ARGS_NAME}}) + set(CPM_DECLARATION_${CPM_ARGS_NAME} "") + CPMAddPackage(${declaration}) + cpm_export_variables(${CPM_ARGS_NAME}) + # checking again to ensure version and option compatibility + cpm_check_if_package_already_added(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}") + return() + endif() + + if(NOT CPM_ARGS_FORCE) + if(CPM_USE_LOCAL_PACKAGES OR CPM_LOCAL_PACKAGES_ONLY) + cpm_find_package(${CPM_ARGS_NAME} "${CPM_ARGS_VERSION}" ${CPM_ARGS_FIND_PACKAGE_ARGUMENTS}) + + if(CPM_PACKAGE_FOUND) + cpm_export_variables(${CPM_ARGS_NAME}) + return() + endif() + + if(CPM_LOCAL_PACKAGES_ONLY) + message( + SEND_ERROR + "${CPM_INDENT} ${CPM_ARGS_NAME} not found via find_package(${CPM_ARGS_NAME} ${CPM_ARGS_VERSION})" + ) + endif() + endif() + endif() + + CPMRegisterPackage("${CPM_ARGS_NAME}" "${CPM_ARGS_VERSION}") + + if(DEFINED CPM_ARGS_GIT_TAG) + set(PACKAGE_INFO "${CPM_ARGS_GIT_TAG}") + elseif(DEFINED CPM_ARGS_SOURCE_DIR) + set(PACKAGE_INFO "${CPM_ARGS_SOURCE_DIR}") + else() + set(PACKAGE_INFO "${CPM_ARGS_VERSION}") + endif() + + if(DEFINED FETCHCONTENT_BASE_DIR) + # respect user's FETCHCONTENT_BASE_DIR if set + set(CPM_FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR}) + else() + set(CPM_FETCHCONTENT_BASE_DIR ${CMAKE_BINARY_DIR}/_deps) + endif() + + cpm_add_patches(${CPM_ARGS_PATCHES}) + + if(DEFINED CPM_ARGS_DOWNLOAD_COMMAND) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS DOWNLOAD_COMMAND ${CPM_ARGS_DOWNLOAD_COMMAND}) + elseif(DEFINED CPM_ARGS_SOURCE_DIR) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS SOURCE_DIR ${CPM_ARGS_SOURCE_DIR}) + if(NOT IS_ABSOLUTE ${CPM_ARGS_SOURCE_DIR}) + # Expand `CPM_ARGS_SOURCE_DIR` relative path. This is important because EXISTS doesn't work + # for relative paths. + get_filename_component( + source_directory ${CPM_ARGS_SOURCE_DIR} REALPATH BASE_DIR ${CMAKE_CURRENT_BINARY_DIR} + ) + else() + set(source_directory ${CPM_ARGS_SOURCE_DIR}) + endif() + if(NOT EXISTS ${source_directory}) + string(TOLOWER ${CPM_ARGS_NAME} lower_case_name) + # remove timestamps so CMake will re-download the dependency + file(REMOVE_RECURSE "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild") + endif() + elseif(CPM_SOURCE_CACHE AND NOT CPM_ARGS_NO_CACHE) + string(TOLOWER ${CPM_ARGS_NAME} lower_case_name) + set(origin_parameters ${CPM_ARGS_UNPARSED_ARGUMENTS}) + list(SORT origin_parameters) + if(CPM_ARGS_CUSTOM_CACHE_KEY) + # Application set a custom unique directory name + set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${CPM_ARGS_CUSTOM_CACHE_KEY}) + elseif(CPM_USE_NAMED_CACHE_DIRECTORIES) + string(SHA1 origin_hash "${origin_parameters};NEW_CACHE_STRUCTURE_TAG") + cpm_get_shortest_hash( + "${CPM_SOURCE_CACHE}/${lower_case_name}" # source cache directory + "${origin_hash}" # Input hash + origin_hash # Computed hash + ) + set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${origin_hash}/${CPM_ARGS_NAME}) + else() + string(SHA1 origin_hash "${origin_parameters}") + cpm_get_shortest_hash( + "${CPM_SOURCE_CACHE}/${lower_case_name}" # source cache directory + "${origin_hash}" # Input hash + origin_hash # Computed hash + ) + set(download_directory ${CPM_SOURCE_CACHE}/${lower_case_name}/${origin_hash}) + endif() + # Expand `download_directory` relative path. This is important because EXISTS doesn't work for + # relative paths. + get_filename_component(download_directory ${download_directory} ABSOLUTE) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS SOURCE_DIR ${download_directory}) + + if(CPM_SOURCE_CACHE) + file(LOCK ${download_directory}/../cmake.lock) + endif() + + if(EXISTS ${download_directory}) + if(CPM_SOURCE_CACHE) + file(LOCK ${download_directory}/../cmake.lock RELEASE) + endif() + + cpm_store_fetch_properties( + ${CPM_ARGS_NAME} "${download_directory}" + "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-build" + ) + cpm_get_fetch_properties("${CPM_ARGS_NAME}") + + if(DEFINED CPM_ARGS_GIT_TAG AND NOT (PATCH_COMMAND IN_LIST CPM_ARGS_UNPARSED_ARGUMENTS)) + # warn if cache has been changed since checkout + cpm_check_git_working_dir_is_clean(${download_directory} ${CPM_ARGS_GIT_TAG} IS_CLEAN) + if(NOT ${IS_CLEAN}) + message( + WARNING "${CPM_INDENT} Cache for ${CPM_ARGS_NAME} (${download_directory}) is dirty" + ) + endif() + endif() + + cpm_add_subdirectory( + "${CPM_ARGS_NAME}" + "${DOWNLOAD_ONLY}" + "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" + "${${CPM_ARGS_NAME}_BINARY_DIR}" + "${CPM_ARGS_EXCLUDE_FROM_ALL}" + "${CPM_ARGS_SYSTEM}" + "${CPM_ARGS_OPTIONS}" + ) + set(PACKAGE_INFO "${PACKAGE_INFO} at ${download_directory}") + + # As the source dir is already cached/populated, we override the call to FetchContent. + set(CPM_SKIP_FETCH TRUE) + cpm_override_fetchcontent( + "${lower_case_name}" SOURCE_DIR "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" + BINARY_DIR "${${CPM_ARGS_NAME}_BINARY_DIR}" + ) + + else() + # Enable shallow clone when GIT_TAG is not a commit hash. Our guess may not be accurate, but + # it should guarantee no commit hash get mis-detected. + if(NOT DEFINED CPM_ARGS_GIT_SHALLOW) + cpm_is_git_tag_commit_hash("${CPM_ARGS_GIT_TAG}" IS_HASH) + if(NOT ${IS_HASH}) + list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS GIT_SHALLOW TRUE) + endif() + endif() + + # remove timestamps so CMake will re-download the dependency + file(REMOVE_RECURSE ${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild) + set(PACKAGE_INFO "${PACKAGE_INFO} to ${download_directory}") + endif() + endif() + + if(NOT "${DOWNLOAD_ONLY}") + cpm_create_module_file(${CPM_ARGS_NAME} "CPMAddPackage(\"${ARGN}\")") + endif() + + if(CPM_PACKAGE_LOCK_ENABLED) + if((CPM_ARGS_VERSION AND NOT CPM_ARGS_SOURCE_DIR) OR CPM_INCLUDE_ALL_IN_PACKAGE_LOCK) + cpm_add_to_package_lock(${CPM_ARGS_NAME} "${ARGN}") + elseif(CPM_ARGS_SOURCE_DIR) + cpm_add_comment_to_package_lock(${CPM_ARGS_NAME} "local directory") + else() + cpm_add_comment_to_package_lock(${CPM_ARGS_NAME} "${ARGN}") + endif() + endif() + + cpm_message( + STATUS "${CPM_INDENT} Adding package ${CPM_ARGS_NAME}@${CPM_ARGS_VERSION} (${PACKAGE_INFO})" + ) + + if(NOT CPM_SKIP_FETCH) + # CMake 3.28 added EXCLUDE, SYSTEM (3.25), and SOURCE_SUBDIR (3.18) to FetchContent_Declare. + # Calling FetchContent_MakeAvailable will then internally forward these options to + # add_subdirectory. Up until these changes, we had to call FetchContent_Populate and + # add_subdirectory separately, which is no longer necessary and has been deprecated as of 3.30. + # A Bug in CMake prevents us to use the non-deprecated functions until 3.30.3. + set(fetchContentDeclareExtraArgs "") + if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.30.3") + if(${CPM_ARGS_EXCLUDE_FROM_ALL}) + list(APPEND fetchContentDeclareExtraArgs EXCLUDE_FROM_ALL) + endif() + if(${CPM_ARGS_SYSTEM}) + list(APPEND fetchContentDeclareExtraArgs SYSTEM) + endif() + if(DEFINED CPM_ARGS_SOURCE_SUBDIR) + list(APPEND fetchContentDeclareExtraArgs SOURCE_SUBDIR ${CPM_ARGS_SOURCE_SUBDIR}) + endif() + # For CMake version <3.28 OPTIONS are parsed in cpm_add_subdirectory + if(CPM_ARGS_OPTIONS AND NOT DOWNLOAD_ONLY) + foreach(OPTION ${CPM_ARGS_OPTIONS}) + cpm_parse_option("${OPTION}") + set(${OPTION_KEY} "${OPTION_VALUE}") + endforeach() + endif() + endif() + cpm_declare_fetch( + "${CPM_ARGS_NAME}" ${fetchContentDeclareExtraArgs} "${CPM_ARGS_UNPARSED_ARGUMENTS}" + ) + + cpm_fetch_package("${CPM_ARGS_NAME}" ${DOWNLOAD_ONLY} populated ${CPM_ARGS_UNPARSED_ARGUMENTS}) + if(CPM_SOURCE_CACHE AND download_directory) + file(LOCK ${download_directory}/../cmake.lock RELEASE) + endif() + if(${populated} AND ${CMAKE_VERSION} VERSION_LESS "3.30.3") + cpm_add_subdirectory( + "${CPM_ARGS_NAME}" + "${DOWNLOAD_ONLY}" + "${${CPM_ARGS_NAME}_SOURCE_DIR}/${CPM_ARGS_SOURCE_SUBDIR}" + "${${CPM_ARGS_NAME}_BINARY_DIR}" + "${CPM_ARGS_EXCLUDE_FROM_ALL}" + "${CPM_ARGS_SYSTEM}" + "${CPM_ARGS_OPTIONS}" + ) + endif() + cpm_get_fetch_properties("${CPM_ARGS_NAME}") + endif() + + set(${CPM_ARGS_NAME}_ADDED YES) + cpm_export_variables("${CPM_ARGS_NAME}") +endfunction() + +# Fetch a previously declared package +macro(CPMGetPackage Name) + if(DEFINED "CPM_DECLARATION_${Name}") + CPMAddPackage(NAME ${Name}) + else() + message(SEND_ERROR "${CPM_INDENT} Cannot retrieve package ${Name}: no declaration available") + endif() +endmacro() + +# export variables available to the caller to the parent scope expects ${CPM_ARGS_NAME} to be set +macro(cpm_export_variables name) + set(${name}_SOURCE_DIR + "${${name}_SOURCE_DIR}" + PARENT_SCOPE + ) + set(${name}_BINARY_DIR + "${${name}_BINARY_DIR}" + PARENT_SCOPE + ) + set(${name}_ADDED + "${${name}_ADDED}" + PARENT_SCOPE + ) + set(CPM_LAST_PACKAGE_NAME + "${name}" + PARENT_SCOPE + ) +endmacro() + +# declares a package, so that any call to CPMAddPackage for the package name will use these +# arguments instead. Previous declarations will not be overridden. +macro(CPMDeclarePackage Name) + if(NOT DEFINED "CPM_DECLARATION_${Name}") + set("CPM_DECLARATION_${Name}" "${ARGN}") + endif() +endmacro() + +function(cpm_add_to_package_lock Name) + if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + cpm_prettify_package_arguments(PRETTY_ARGN false ${ARGN}) + file(APPEND ${CPM_PACKAGE_LOCK_FILE} "# ${Name}\nCPMDeclarePackage(${Name}\n${PRETTY_ARGN})\n") + endif() +endfunction() + +function(cpm_add_comment_to_package_lock Name) + if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + cpm_prettify_package_arguments(PRETTY_ARGN true ${ARGN}) + file(APPEND ${CPM_PACKAGE_LOCK_FILE} + "# ${Name} (unversioned)\n# CPMDeclarePackage(${Name}\n${PRETTY_ARGN}#)\n" + ) + endif() +endfunction() + +# includes the package lock file if it exists and creates a target `cpm-update-package-lock` to +# update it +macro(CPMUsePackageLock file) + if(NOT CPM_DONT_CREATE_PACKAGE_LOCK) + get_filename_component(CPM_ABSOLUTE_PACKAGE_LOCK_PATH ${file} ABSOLUTE) + if(EXISTS ${CPM_ABSOLUTE_PACKAGE_LOCK_PATH}) + include(${CPM_ABSOLUTE_PACKAGE_LOCK_PATH}) + endif() + if(NOT TARGET cpm-update-package-lock) + add_custom_target( + cpm-update-package-lock COMMAND ${CMAKE_COMMAND} -E copy ${CPM_PACKAGE_LOCK_FILE} + ${CPM_ABSOLUTE_PACKAGE_LOCK_PATH} + ) + endif() + set(CPM_PACKAGE_LOCK_ENABLED true) + endif() +endmacro() + +# registers a package that has been added to CPM +function(CPMRegisterPackage PACKAGE VERSION) + list(APPEND CPM_PACKAGES ${PACKAGE}) + set(CPM_PACKAGES + ${CPM_PACKAGES} + CACHE INTERNAL "" + ) + set("CPM_PACKAGE_${PACKAGE}_VERSION" + ${VERSION} + CACHE INTERNAL "" + ) +endfunction() + +# retrieve the current version of the package to ${OUTPUT} +function(CPMGetPackageVersion PACKAGE OUTPUT) + set(${OUTPUT} + "${CPM_PACKAGE_${PACKAGE}_VERSION}" + PARENT_SCOPE + ) +endfunction() + +# declares a package in FetchContent_Declare +function(cpm_declare_fetch PACKAGE) + if(${CPM_DRY_RUN}) + cpm_message(STATUS "${CPM_INDENT} Package not declared (dry run)") + return() + endif() + + FetchContent_Declare(${PACKAGE} ${ARGN}) +endfunction() + +# returns properties for a package previously defined by cpm_declare_fetch +function(cpm_get_fetch_properties PACKAGE) + if(${CPM_DRY_RUN}) + return() + endif() + + set(${PACKAGE}_SOURCE_DIR + "${CPM_PACKAGE_${PACKAGE}_SOURCE_DIR}" + PARENT_SCOPE + ) + set(${PACKAGE}_BINARY_DIR + "${CPM_PACKAGE_${PACKAGE}_BINARY_DIR}" + PARENT_SCOPE + ) +endfunction() + +function(cpm_store_fetch_properties PACKAGE source_dir binary_dir) + if(${CPM_DRY_RUN}) + return() + endif() + + set(CPM_PACKAGE_${PACKAGE}_SOURCE_DIR + "${source_dir}" + CACHE INTERNAL "" + ) + set(CPM_PACKAGE_${PACKAGE}_BINARY_DIR + "${binary_dir}" + CACHE INTERNAL "" + ) +endfunction() + +# adds a package as a subdirectory if viable, according to provided options +function( + cpm_add_subdirectory + PACKAGE + DOWNLOAD_ONLY + SOURCE_DIR + BINARY_DIR + EXCLUDE + SYSTEM + OPTIONS +) + + if(NOT DOWNLOAD_ONLY AND EXISTS ${SOURCE_DIR}/CMakeLists.txt) + set(addSubdirectoryExtraArgs "") + if(EXCLUDE) + list(APPEND addSubdirectoryExtraArgs EXCLUDE_FROM_ALL) + endif() + if("${SYSTEM}" AND "${CMAKE_VERSION}" VERSION_GREATER_EQUAL "3.25") + # https://cmake.org/cmake/help/latest/prop_dir/SYSTEM.html#prop_dir:SYSTEM + list(APPEND addSubdirectoryExtraArgs SYSTEM) + endif() + if(OPTIONS) + foreach(OPTION ${OPTIONS}) + cpm_parse_option("${OPTION}") + set(${OPTION_KEY} "${OPTION_VALUE}") + endforeach() + endif() + set(CPM_OLD_INDENT "${CPM_INDENT}") + set(CPM_INDENT "${CPM_INDENT} ${PACKAGE}:") + add_subdirectory(${SOURCE_DIR} ${BINARY_DIR} ${addSubdirectoryExtraArgs}) + set(CPM_INDENT "${CPM_OLD_INDENT}") + endif() +endfunction() + +# downloads a previously declared package via FetchContent and exports the variables +# `${PACKAGE}_SOURCE_DIR` and `${PACKAGE}_BINARY_DIR` to the parent scope +function(cpm_fetch_package PACKAGE DOWNLOAD_ONLY populated) + set(${populated} + FALSE + PARENT_SCOPE + ) + if(${CPM_DRY_RUN}) + cpm_message(STATUS "${CPM_INDENT} Package ${PACKAGE} not fetched (dry run)") + return() + endif() + + FetchContent_GetProperties(${PACKAGE}) + + string(TOLOWER "${PACKAGE}" lower_case_name) + + if(NOT ${lower_case_name}_POPULATED) + if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.30.3") + if(DOWNLOAD_ONLY) + # MakeAvailable will call add_subdirectory internally which is not what we want when + # DOWNLOAD_ONLY is set. Populate will only download the dependency without adding it to the + # build + FetchContent_Populate( + ${PACKAGE} + SOURCE_DIR "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-src" + BINARY_DIR "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-build" + SUBBUILD_DIR "${CPM_FETCHCONTENT_BASE_DIR}/${lower_case_name}-subbuild" + ${ARGN} + ) + else() + FetchContent_MakeAvailable(${PACKAGE}) + endif() + else() + FetchContent_Populate(${PACKAGE}) + endif() + set(${populated} + TRUE + PARENT_SCOPE + ) + endif() + + cpm_store_fetch_properties( + ${CPM_ARGS_NAME} ${${lower_case_name}_SOURCE_DIR} ${${lower_case_name}_BINARY_DIR} + ) + + set(${PACKAGE}_SOURCE_DIR + ${${lower_case_name}_SOURCE_DIR} + PARENT_SCOPE + ) + set(${PACKAGE}_BINARY_DIR + ${${lower_case_name}_BINARY_DIR} + PARENT_SCOPE + ) +endfunction() + +# splits a package option +function(cpm_parse_option OPTION) + string(REGEX MATCH "^[^ ]+" OPTION_KEY "${OPTION}") + string(LENGTH "${OPTION}" OPTION_LENGTH) + string(LENGTH "${OPTION_KEY}" OPTION_KEY_LENGTH) + if(OPTION_KEY_LENGTH STREQUAL OPTION_LENGTH) + # no value for key provided, assume user wants to set option to "ON" + set(OPTION_VALUE "ON") + else() + math(EXPR OPTION_KEY_LENGTH "${OPTION_KEY_LENGTH}+1") + string(SUBSTRING "${OPTION}" "${OPTION_KEY_LENGTH}" "-1" OPTION_VALUE) + endif() + set(OPTION_KEY + "${OPTION_KEY}" + PARENT_SCOPE + ) + set(OPTION_VALUE + "${OPTION_VALUE}" + PARENT_SCOPE + ) +endfunction() + +# guesses the package version from a git tag +function(cpm_get_version_from_git_tag GIT_TAG RESULT) + string(LENGTH ${GIT_TAG} length) + if(length EQUAL 40) + # GIT_TAG is probably a git hash + set(${RESULT} + 0 + PARENT_SCOPE + ) + else() + string(REGEX MATCH "v?([0123456789.]*).*" _ ${GIT_TAG}) + set(${RESULT} + ${CMAKE_MATCH_1} + PARENT_SCOPE + ) + endif() +endfunction() + +# guesses if the git tag is a commit hash or an actual tag or a branch name. +function(cpm_is_git_tag_commit_hash GIT_TAG RESULT) + string(LENGTH "${GIT_TAG}" length) + # full hash has 40 characters, and short hash has at least 7 characters. + if(length LESS 7 OR length GREATER 40) + set(${RESULT} + 0 + PARENT_SCOPE + ) + else() + if(${GIT_TAG} MATCHES "^[a-fA-F0-9]+$") + set(${RESULT} + 1 + PARENT_SCOPE + ) + else() + set(${RESULT} + 0 + PARENT_SCOPE + ) + endif() + endif() +endfunction() + +function(cpm_prettify_package_arguments OUT_VAR IS_IN_COMMENT) + set(oneValueArgs + NAME + FORCE + VERSION + GIT_TAG + DOWNLOAD_ONLY + GITHUB_REPOSITORY + GITLAB_REPOSITORY + BITBUCKET_REPOSITORY + GIT_REPOSITORY + SOURCE_DIR + FIND_PACKAGE_ARGUMENTS + NO_CACHE + SYSTEM + GIT_SHALLOW + EXCLUDE_FROM_ALL + SOURCE_SUBDIR + ) + set(multiValueArgs URL OPTIONS DOWNLOAD_COMMAND) + cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + foreach(oneArgName ${oneValueArgs}) + if(DEFINED CPM_ARGS_${oneArgName}) + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + if(${oneArgName} STREQUAL "SOURCE_DIR") + string(REPLACE ${CMAKE_SOURCE_DIR} "\${CMAKE_SOURCE_DIR}" CPM_ARGS_${oneArgName} + ${CPM_ARGS_${oneArgName}} + ) + endif() + string(APPEND PRETTY_OUT_VAR " ${oneArgName} ${CPM_ARGS_${oneArgName}}\n") + endif() + endforeach() + foreach(multiArgName ${multiValueArgs}) + if(DEFINED CPM_ARGS_${multiArgName}) + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + string(APPEND PRETTY_OUT_VAR " ${multiArgName}\n") + foreach(singleOption ${CPM_ARGS_${multiArgName}}) + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + string(APPEND PRETTY_OUT_VAR " \"${singleOption}\"\n") + endforeach() + endif() + endforeach() + + if(NOT "${CPM_ARGS_UNPARSED_ARGUMENTS}" STREQUAL "") + if(${IS_IN_COMMENT}) + string(APPEND PRETTY_OUT_VAR "#") + endif() + string(APPEND PRETTY_OUT_VAR " ") + foreach(CPM_ARGS_UNPARSED_ARGUMENT ${CPM_ARGS_UNPARSED_ARGUMENTS}) + string(APPEND PRETTY_OUT_VAR " ${CPM_ARGS_UNPARSED_ARGUMENT}") + endforeach() + string(APPEND PRETTY_OUT_VAR "\n") + endif() + + set(${OUT_VAR} + ${PRETTY_OUT_VAR} + PARENT_SCOPE + ) + +endfunction() diff --git a/code/volumn_codes/vol9/chrome_design/once_callback/CMakeLists.txt b/code/volumn_codes/vol9/chrome_design/once_callback/CMakeLists.txt new file mode 100644 index 000000000..2841d5b8f --- /dev/null +++ b/code/volumn_codes/vol9/chrome_design/once_callback/CMakeLists.txt @@ -0,0 +1,4 @@ +# once_callback: header-only interface library +add_library(once_callback INTERFACE) +target_include_directories(once_callback INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/..) +target_compile_features(once_callback INTERFACE cxx_std_23) diff --git a/code/volumn_codes/vol9/chrome_design/once_callback/once_callback.hpp b/code/volumn_codes/vol9/chrome_design/once_callback/once_callback.hpp new file mode 100644 index 000000000..c5449c333 --- /dev/null +++ b/code/volumn_codes/vol9/chrome_design/once_callback/once_callback.hpp @@ -0,0 +1,117 @@ +/** + * @file once_callback.hpp + * @author Charliechen114514 (chengh1922@mails.jlu.edu.cn) + * @brief Once Callback single file + * @version 0.1 + * @date 2026-05-02 + * + * @copyright Copyright (c) 2026 + * + */ + +#pragma once +#include "cancel_token/cancel_token.hpp" +#include + +namespace tamcpp::chrome { + +/* + * OK, we request the newest gcc to finish the issue + * update your gcc to try new feature! + */ +static_assert(__cpp_lib_move_only_function >= 202110L); + +/** + * @brief + * + * @tparam filled here the function, promise to callback once + */ +template class OnceCallback; + +template +concept not_the_same_t = !std::is_same_v, T>; + +template +class OnceCallback // Specialization of Functional like +{ + public: + using FuncSig = ReturnType(FuncArgs...); + + private: + enum class Status { + kEmpty, // Null when construction with no lambda or func specified + kValid, // validate for usage + kConsumed // Has been callbacked + } status_ = Status::kEmpty; + + std::move_only_function func_; + std::shared_ptr token_; + + /** + * @brief Disabled the copy + * + */ + OnceCallback(const OnceCallback&) = delete; + OnceCallback& operator=(const OnceCallback&) = delete; + + public: + template + requires not_the_same_t + explicit OnceCallback(Functor&& function) + : status_(Status::kValid), func_(std::move(function)) {} + + explicit OnceCallback() = default; + + OnceCallback(OnceCallback&& other) noexcept + : status_(other.status_), func_(std::move(other.func_)), token_(std::move(other.token_)) { + other.status_ = Status::kEmpty; + } + OnceCallback& operator=(OnceCallback&& other) noexcept { + if (this != &other) { + status_ = other.status_; + func_ = std::move(other.func_); + token_ = std::move(other.token_); + other.status_ = Status::kEmpty; + } + return *this; + } + + void set_token(std::shared_ptr token) { token_ = std::move(token); } + + template auto then(Next&& next) &&; + + /** + * @brief + * + * @tparam deducing this features, no need to be filled + * @param self + * @param args + * @return ReturnType + */ + template auto run(this Self&& self, FuncArgs&&... args) -> ReturnType; + + /** + * @brief Check Issue + * + * @return true + * @return false + */ + [[nodiscard]] bool is_cancelled() const noexcept { + if (status_ != Status::kValid) return true; + if (token_ && !token_->is_valid()) return true; + return false; + } + [[nodiscard]] bool maybe_valid() const noexcept { return !is_cancelled(); } + [[nodiscard]] bool is_null() const noexcept { return status_ == Status::kEmpty; } + explicit operator bool() const noexcept { return !is_null() && !is_cancelled(); } + + private: + ReturnType impl_run(FuncArgs... args); +}; + +template +auto bind_once(F&& funtor, BoundArgs&&... args); + +} // namespace tamcpp::chrome + +#include "once_callback_impl.hpp" diff --git a/code/volumn_codes/vol9/chrome_design/once_callback/once_callback_impl.hpp b/code/volumn_codes/vol9/chrome_design/once_callback/once_callback_impl.hpp new file mode 100644 index 000000000..ec4e97710 --- /dev/null +++ b/code/volumn_codes/vol9/chrome_design/once_callback/once_callback_impl.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include "once_callback.hpp" +#include +#include +#include + +namespace tamcpp::chrome { +template +template +auto OnceCallback::run(this Self&& self, FuncArgs&&... args) + -> ReturnType { + static_assert(!std::is_lvalue_reference_v, + "once_callback::run() must be called on an rvalue. " + "Use std::move(cb).run(...) instead."); + return std::forward(self).impl_run(std::forward(args)...); +} + +template +ReturnType OnceCallback::impl_run(FuncArgs... args) { + assert(status_ == Status::kValid); + + // Cancelled: consume without executing + if (token_ && !token_->is_valid()) { + status_ = Status::kConsumed; + func_ = nullptr; + if constexpr (std::is_void_v) { + return; + } else { + throw std::bad_function_call{}; + } + } + + auto functor = std::move(func_); + func_ = nullptr; + status_ = Status::kConsumed; + + if constexpr (std::is_void_v) { + functor(std::forward(args)...); + } else { + return functor(std::forward(args)...); + } +} + +template +auto bind_once(F&& funtor, BoundArgs&&... args) { + return OnceCallback( + [f = std::forward(funtor), + ... bound = std::forward(args)](auto&&... call_args) mutable -> decltype(auto) { + return std::invoke(std::move(f), std::move(bound)..., + std::forward(call_args)...); + }); +} + +template +template +auto OnceCallback::then(Next&& next) && { + using NextType = std::decay_t; + + if constexpr (std::is_void_v) { + using NextRet = std::invoke_result_t; + return OnceCallback( + [self = std::move(*this), + cont = std::forward(next)] + (FuncArgs... args) mutable -> NextRet { + std::move(self).run(std::forward(args)...); + return std::invoke(std::move(cont)); + }); + } else { + using NextRet = std::invoke_result_t; + return OnceCallback( + [self = std::move(*this), + cont = std::forward(next)] + (FuncArgs... args) mutable -> NextRet { + auto mid = std::move(self).run(std::forward(args)...); + return std::invoke(std::move(cont), std::move(mid)); + }); + } +} + +} // namespace tamcpp::chrome diff --git a/code/volumn_codes/vol9/chrome_design/test/CMakeLists.txt b/code/volumn_codes/vol9/chrome_design/test/CMakeLists.txt new file mode 100644 index 000000000..1cd339a20 --- /dev/null +++ b/code/volumn_codes/vol9/chrome_design/test/CMakeLists.txt @@ -0,0 +1,7 @@ +CPMAddPackage("gh:catchorg/Catch2@3.7.1") + +add_executable(test_once_callback test_once_callback.cpp) +target_link_libraries(test_once_callback PRIVATE once_callback Catch2::Catch2WithMain) +target_compile_options(test_once_callback PRIVATE -Wall -Wextra -Wpedantic) + +add_test(NAME test_once_callback COMMAND test_once_callback) diff --git a/code/volumn_codes/vol9/chrome_design/test/test_once_callback.cpp b/code/volumn_codes/vol9/chrome_design/test/test_once_callback.cpp new file mode 100644 index 000000000..81ff1582b --- /dev/null +++ b/code/volumn_codes/vol9/chrome_design/test/test_once_callback.cpp @@ -0,0 +1,104 @@ +#include +#include +#include +#include + +using namespace tamcpp::chrome; + +TEST_CASE("non-void return", "[once_callback]") { + OnceCallback cb([](int a, int b) { return a + b; }); + int result = std::move(cb).run(3, 4); + REQUIRE(result == 7); +} + +TEST_CASE("void return", "[once_callback]") { + bool called = false; + OnceCallback cb([&called] { called = true; }); + std::move(cb).run(); + REQUIRE(called); +} + +TEST_CASE("move-only capture", "[once_callback]") { + auto ptr = std::make_unique(42); + OnceCallback cb([p = std::move(ptr)] { return *p; }); + int result = std::move(cb).run(); + REQUIRE(result == 42); +} + +TEST_CASE("move semantics: source becomes null", "[once_callback]") { + OnceCallback cb([] { return 1; }); + OnceCallback cb2 = std::move(cb); + REQUIRE(cb.is_null()); + + int result = std::move(cb2).run(); + REQUIRE(result == 1); +} + +TEST_CASE("is_cancelled respects cancel token", "[once_callback]") { + auto token = std::make_shared(); + OnceCallback cb([] {}); + cb.set_token(token); + + REQUIRE_FALSE(cb.is_cancelled()); + token->invalidate(); + REQUIRE(cb.is_cancelled()); +} + +TEST_CASE("cancelled void callback does not execute", "[once_callback]") { + auto token = std::make_shared(); + bool called = false; + OnceCallback cb([&called] { called = true; }); + cb.set_token(token); + token->invalidate(); + + std::move(cb).run(); + REQUIRE_FALSE(called); +} + +TEST_CASE("cancelled non-void callback throws", "[once_callback]") { + auto token = std::make_shared(); + OnceCallback cb([] { return 1; }); + cb.set_token(token); + token->invalidate(); + + REQUIRE_THROWS_AS(std::move(cb).run(), std::bad_function_call); +} + +TEST_CASE("bind_once basic", "[bind_once]") { + auto bound = bind_once([](int a, int b) { return a * b; }, 5); + int result = std::move(bound).run(8); + REQUIRE(result == 40); +} + +TEST_CASE("bind_once with member function", "[bind_once]") { + struct Calc { + int multiply(int a, int b) { return a * b; } + }; + Calc calc; + auto bound = bind_once(&Calc::multiply, &calc, 5); + int result = std::move(bound).run(8); + REQUIRE(result == 40); +} + +TEST_CASE("then chains two callbacks", "[then]") { + auto cb = OnceCallback([](int x) { return x * 2; }) + .then([](int x) { return x + 10; }); + int result = std::move(cb).run(5); + REQUIRE(result == 20); // 5 * 2 + 10 +} + +TEST_CASE("then multi-level pipeline", "[then]") { + auto pipeline = OnceCallback([](int x) { return x * 2; }) + .then([](int x) { return x + 10; }) + .then([](int x) { return std::to_string(x); }); + std::string result = std::move(pipeline).run(5); + REQUIRE(result == "20"); // (5*2)+10 = "20" +} + +TEST_CASE("then with void first callback", "[then]") { + int value = 0; + auto cb = OnceCallback([&value](int x) { value = x; }) + .then([&value] { return value * 3; }); + int result = std::move(cb).run(7); + REQUIRE(result == 21); +} diff --git a/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/01_move_semantics.cpp b/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/01_move_semantics.cpp new file mode 100644 index 000000000..668b1553b --- /dev/null +++ b/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/01_move_semantics.cpp @@ -0,0 +1,83 @@ +// 移动语义、std::move 与完美转发 +// 来源:OnceCallback 前置知识速查 (pre-00) +// 编译:g++ -std=c++17 -Wall -Wextra 01_move_semantics.cpp -o 01_move_semantics + +#include +#include +#include +#include + +// --- 移动语义示例:Buffer 类 --- + +class Buffer { + int* data_; + std::size_t size_; +public: + explicit Buffer(std::size_t n) : data_(new int[n]), size_(n) { + std::cout << " Buffer(" << n << "): allocated\n"; + } + + // 移动构造:偷走 other 的资源 + Buffer(Buffer&& other) noexcept + : data_(other.data_), size_(other.size_) { + other.data_ = nullptr; + other.size_ = 0; + std::cout << " Buffer(move): resource stolen\n"; + } + + ~Buffer() { + delete[] data_; + std::cout << " Buffer::~Buffer(): " << (data_ ? "freed" : "null") << "\n"; + } + + std::size_t size() const { return size_; } +}; + +// --- 完美转发示例 --- + +void target(int& x) { + std::cout << " target(lvalue ref): " << x << "\n"; +} + +void target(int&& x) { + std::cout << " target(rvalue ref): " << x << "\n"; +} + +template +void wrapper(T&& arg) { + // std::forward 保持 arg 的原始值类别 + target(std::forward(arg)); +} + +// --- 可变参数模板示例 --- + +template +void print_all(Types... args) { + std::cout << " print_all: " << sizeof...(Types) << " arguments\n"; + // C++17 折叠表达式打印 + ((std::cout << " " << args << "\n"), ...); +} + +int main() { + std::cout << "=== 移动语义 ===\n"; + { + Buffer a(100); + Buffer b = std::move(a); + std::cout << " b.size() = " << b.size() << "\n"; + } + + std::cout << "\n=== 完美转发 ===\n"; + { + int x = 10; + wrapper(x); // arg 是左值引用,forward 返回左值引用 + wrapper(20); // arg 是右值引用,forward 返回右值引用 + } + + std::cout << "\n=== 可变参数模板 ===\n"; + { + print_all(1, 2.5, std::string("hello")); + print_all(); // 空参数包 + } + + return 0; +} diff --git a/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/02_smart_pointers.cpp b/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/02_smart_pointers.cpp new file mode 100644 index 000000000..6bbf523a3 --- /dev/null +++ b/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/02_smart_pointers.cpp @@ -0,0 +1,39 @@ +// 智能指针:unique_ptr 与 shared_ptr +// 来源:OnceCallback 前置知识速查 (pre-00) +// 编译:g++ -std=c++17 -Wall -Wextra 02_smart_pointers.cpp -o 02_smart_pointers + +#include +#include +#include + +int main() { + std::cout << "=== std::unique_ptr:独占所有权 ===\n"; + { + auto p = std::make_unique(42); + std::cout << " *p = " << *p << "\n"; + // auto p2 = p; // 编译错误:不可拷贝 + auto p3 = std::move(p); // OK:移动转移所有权 + std::cout << " after move, p3 = " << *p3 << "\n"; + std::cout << " p is " << (p ? "not null" : "nullptr") << "\n"; + } + + std::cout << "\n=== std::shared_ptr:共享所有权 ===\n"; + { + auto p1 = std::make_shared("hello"); + auto p2 = p1; // OK:拷贝,引用计数 +1 + std::cout << " *p1 = " << *p1 << "\n"; + std::cout << " *p2 = " << *p2 << "\n"; + std::cout << " use_count = " << p1.use_count() << "\n"; + } + + std::cout << "\n=== unique_ptr 捕获到 lambda (move-only) ===\n"; + { + auto ptr = std::make_unique(99); + // 移动捕获 unique_ptr 到 lambda + auto f = [p = std::move(ptr)]() { return *p; }; + std::cout << " f() = " << f() << "\n"; + std::cout << " ptr is " << (ptr ? "not null" : "nullptr") << "\n"; + } + + return 0; +} diff --git a/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/03_atomic_memory_order.cpp b/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/03_atomic_memory_order.cpp new file mode 100644 index 000000000..931ccd658 --- /dev/null +++ b/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/03_atomic_memory_order.cpp @@ -0,0 +1,55 @@ +// std::atomic 与 memory_order +// 来源:OnceCallback 前置知识速查 (pre-00) +// 编译:g++ -std=c++17 -Wall -Wextra 03_atomic_memory_order.cpp -o 03_atomic_memory_order -pthread + +#include +#include +#include + +int main() { + std::cout << "=== std::atomic 基本操作 ===\n"; + { + std::atomic flag{true}; + std::cout << " initial: " << flag.load() << "\n"; + + flag.store(false, std::memory_order_release); + std::cout << " after store: " << flag.load(std::memory_order_acquire) << "\n"; + } + + std::cout << "\n=== 线程间 acquire/release 同步 ===\n"; + { + std::atomic ready{false}; + int data = 0; + + // 线程 A:写入数据,然后设 ready = true + std::thread producer([&]() { + data = 42; + ready.store(true, std::memory_order_release); + }); + + // 线程 B:等待 ready,然后读取数据 + std::thread consumer([&]() { + while (!ready.load(std::memory_order_acquire)) { + // 自旋等待 + } + std::cout << " consumer sees data = " << data << "\n"; + }); + + producer.join(); + consumer.join(); + } + + std::cout << "\n=== enum class ===\n"; + { + enum class Status : unsigned char { + kEmpty, + kValid, + kConsumed + }; + Status s = Status::kValid; + // int y = s; // 编译错误:不可隐式转换 + std::cout << " Status value (cast) = " << static_cast(s) << "\n"; + } + + return 0; +} diff --git a/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/04_lambda_basics.cpp b/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/04_lambda_basics.cpp new file mode 100644 index 000000000..382073ca1 --- /dev/null +++ b/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/04_lambda_basics.cpp @@ -0,0 +1,46 @@ +// Lambda 基础:捕获模式与泛型 lambda +// 来源:OnceCallback 前置知识速查 (pre-00) +// 编译:g++ -std=c++17 -Wall -Wextra 04_lambda_basics.cpp -o 04_lambda_basics + +#include +#include +#include + +int main() { + std::cout << "=== Lambda 捕获模式 ===\n"; + { + int x = 10; + + auto f1 = [x]() { return x; }; // 值捕获 + auto f2 = [&x]() { return x; }; // 引用捕获 + auto f3 = [p = std::make_unique(42)]() { // 初始化捕获 (C++14) + return *p; + }; + + std::cout << " value capture: " << f1() << "\n"; + std::cout << " ref capture: " << f2() << "\n"; + std::cout << " init capture: " << f3() << "\n"; + } + + std::cout << "\n=== 泛型 lambda (C++14) ===\n"; + { + auto generic = [](auto x, auto y) { return x + y; }; + std::cout << " generic(1, 2) = " << generic(1, 2) << "\n"; + std::cout << " generic(1.5, 2.5) = " << generic(1.5, 2.5) << "\n"; + std::cout << " generic(std::string(\"hello\"), std::string(\" world\")) = " + << generic(std::string("hello"), std::string(" world")) << "\n"; + } + + std::cout << "\n=== [[nodiscard]] 属性 (C++17) ===\n"; + { + struct Checker { + [[nodiscard]] bool is_valid() const noexcept { return true; } + }; + Checker c; + // c.is_valid(); // 编译器警告:返回值被忽略 + bool ok = c.is_valid(); + std::cout << " is_valid() = " << ok << "\n"; + } + + return 0; +} diff --git a/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/05_lambda_advanced.cpp b/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/05_lambda_advanced.cpp new file mode 100644 index 000000000..2b6ed3395 --- /dev/null +++ b/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/05_lambda_advanced.cpp @@ -0,0 +1,74 @@ +// Lambda 高级特性:mutable、初始化捕获、C++17/C++20 bind +// 来源:OnceCallback 前置知识(三)(pre-03) +// 编译:g++ -std=c++20 -Wall -Wextra 05_lambda_advanced.cpp -o 05_lambda_advanced + +#include +#include +#include +#include +#include + +int main() { + std::cout << "=== mutable lambda ===\n"; + { + int x = 10; + + auto f2 = [x]() mutable { + x++; + return x; + }; + std::cout << " first call: " << f2() << "\n"; // 11 + std::cout << " second call: " << f2() << "\n"; // 12 + } + + std::cout << "\n=== 初始化捕获 (C++14) ===\n"; + { + auto ptr = std::make_unique(42); + auto f1 = [p = std::move(ptr)]() { return *p; }; // 移动捕获 + std::cout << " move capture: " << f1() << "\n"; + + std::string s = "hello"; + auto f2 = [len = s.size()]() { return len; }; // 存储计算结果 + std::cout << " computed capture: " << f2() << "\n"; + + auto f3 = [counter = 0]() mutable { return ++counter; }; // 新变量 + std::cout << " new var capture: " << f3() << ", " << f3() << "\n"; + } + + std::cout << "\n=== 旧版 bind (C++17): tuple + apply ===\n"; + { + auto add = [](int a, int b, int c) { return a + b + c; }; + + auto bind_old = [](auto&& f, auto&&... args) { + return [f = std::forward(f), + tup = std::make_tuple(std::forward(args)...)] + (auto&&... call_args) mutable -> decltype(auto) { + return std::apply([&](auto&... bound) -> decltype(auto) { + return f(bound..., std::forward(call_args)...); + }, tup); + }; + }; + + auto bound = bind_old(add, 10, 20); + std::cout << " bind_old(10, 20)(30) = " << bound(30) << "\n"; + } + + std::cout << "\n=== 新版 bind (C++20): capture pack expansion ===\n"; + { + auto bind_new = [](auto&& f, auto&&... args) { + return [f = std::forward(f), + ...bound = std::forward(args)] + (auto&&... call_args) mutable -> decltype(auto) { + return std::invoke(std::move(f), + std::move(bound)..., + std::forward(call_args)...); + }; + }; + + auto add = [](int a, int b, int c) { return a + b + c; }; + auto bound = bind_new(add, 10, 20); + std::cout << " bind_new(10, 20)(30) = " << bound(30) << "\n"; + } + + return 0; +} diff --git a/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/06_type_traits.cpp b/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/06_type_traits.cpp new file mode 100644 index 000000000..f0f00f948 --- /dev/null +++ b/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/06_type_traits.cpp @@ -0,0 +1,95 @@ +// 类型特征(Type Traits)与 if constexpr +// 来源:OnceCallback 前置知识速查 (pre-00) +// 编译:g++ -std=c++17 -Wall -Wextra 06_type_traits.cpp -o 06_type_traits + +#include +#include +#include + +// --- if constexpr 示例 --- + +void perform_action() { std::cout << " action performed\n"; } +int perform_action_int() { return 42; } + +template +R do_something() { + if constexpr (std::is_void_v) { + perform_action(); + return; + } else { + return perform_action_int(); + } +} + +// --- decltype(auto) 示例 --- + +int global_val = 10; + +auto f_auto() { + return global_val; // 返回 int(丢掉引用) +} + +decltype(auto) f_decltype_auto() { + return global_val; // 返回 int&(保留引用) +} + +// --- Ref-qualified 成员函数 --- + +class Widget { +public: + void process() & { + std::cout << " process() &: called on lvalue\n"; + } + void process() && { + std::cout << " process() &&: called on rvalue\n"; + } +}; + +int main() { + std::cout << "=== Type Traits ===\n"; + { + using T1 = std::decay_t; + static_assert(std::is_same_v); + std::cout << " decay_t = int: OK\n"; + + static_assert(std::is_same_v); + static_assert(!std::is_same_v); + std::cout << " is_same_v checks: OK\n"; + + static_assert(std::is_lvalue_reference_v); + static_assert(!std::is_lvalue_reference_v); + static_assert(!std::is_lvalue_reference_v); + std::cout << " is_lvalue_reference_v checks: OK\n"; + + static_assert(std::is_void_v); + static_assert(!std::is_void_v); + std::cout << " is_void_v checks: OK\n"; + } + + std::cout << "\n=== if constexpr ===\n"; + { + do_something(); + int result = do_something(); + std::cout << " do_something() = " << result << "\n"; + } + + std::cout << "\n=== decltype(auto) ===\n"; + { + auto a = f_auto(); // int + decltype(auto) b = f_decltype_auto(); // int& + std::cout << " f_auto() returns int: " << a << "\n"; + std::cout << " f_decltype_auto() returns int&: " << b << "\n"; + b = 99; + std::cout << " after b = 99, global_val = " << global_val << "\n"; + } + + std::cout << "\n=== Ref-qualified 成员函数 ===\n"; + { + Widget w; + w.process(); // 调用 & 版本 + std::move(w).process(); // 调用 && 版本 + Widget().process(); // 调用 && 版本 + } + + return 0; +} diff --git a/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/07_function_type_specialization.cpp b/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/07_function_type_specialization.cpp new file mode 100644 index 000000000..cf894be35 --- /dev/null +++ b/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/07_function_type_specialization.cpp @@ -0,0 +1,76 @@ +// 函数类型与模板偏特化 +// 来源:OnceCallback 前置知识(一)(pre-01) +// 编译:g++ -std=c++17 -Wall -Wextra 07_function_type_specialization.cpp -o 07_function_type_specialization + +#include +#include +#include +#include + +// --- 函数类型验证 --- + +static_assert(std::is_function_v); +static_assert(!std::is_pointer_v); +static_assert(std::is_pointer_v); + +// --- 主模板 + 偏特化模式 --- + +// 主模板:只有声明,没有定义 +template +struct FuncTraits; + +// 偏特化:拆解函数类型 R(Args...) +template +struct FuncTraits { + using ReturnType = R; + using ArgsTuple = std::tuple; + static constexpr std::size_t kArity = sizeof...(Args); +}; + +// --- std::function 风格的简化实现 --- + +template class SimpleFunction; // 主模板 + +template +class SimpleFunction { + // 偏特化:实际的实现在这里 +public: + using Signature = R(Args...); + using ReturnType = R; + static constexpr std::size_t kArity = sizeof...(Args); +}; + +int main() { + std::cout << "=== 函数类型 static_assert ===\n"; + std::cout << " int(int, int) is a function type\n"; + std::cout << " int(int, int) is NOT a pointer\n"; + std::cout << " int(*)(int, int) IS a pointer\n"; + + std::cout << "\n=== FuncTraits 验证 ===\n"; + static_assert(std::is_same_v::ReturnType, int>); + static_assert(std::is_same_v::ReturnType, void>); + static_assert(FuncTraits::kArity == 3); + + std::cout << " FuncTraits::ReturnType == int\n"; + std::cout << " FuncTraits::ReturnType == void\n"; + std::cout << " FuncTraits::kArity == 3\n"; + + std::cout << "\n=== 更复杂的类型验证 ===\n"; + static_assert(std::is_same_v< + FuncTraits::ReturnType, + std::string>); + static_assert(std::is_same_v< + FuncTraits::ArgsTuple, + std::tuple>); + + std::cout << " FuncTraits::ReturnType == std::string\n"; + std::cout << " FuncTraits::ArgsTuple == std::tuple\n"; + + std::cout << "\n=== SimpleFunction 偏特化模式 ===\n"; + static_assert(SimpleFunction::kArity == 2); + static_assert(std::is_same_v::ReturnType, double>); + std::cout << " SimpleFunction::kArity == 2\n"; + std::cout << " SimpleFunction::ReturnType == double\n"; + + return 0; +} diff --git a/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/08_invoke.cpp b/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/08_invoke.cpp new file mode 100644 index 000000000..dd0141472 --- /dev/null +++ b/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/08_invoke.cpp @@ -0,0 +1,97 @@ +// std::invoke 与统一调用协议 +// 来源:OnceCallback 前置知识(二)(pre-02) +// 编译:g++ -std=c++17 -Wall -Wextra 08_invoke.cpp -o 08_invoke + +#include +#include +#include +#include + +// --- 普通函数 --- + +int add(int a, int b) { return a + b; } + +// --- 仿函数 --- + +struct Adder { + int operator()(int a, int b) { return a + b; } +}; + +// --- 成员函数与数据成员 --- + +struct Calculator { + int multiply(int a, int b) { return a * b; } +}; + +struct Point { + double x, y; +}; + +int main() { + std::cout << "=== 普通函数指针 ===\n"; + { + int (*fp)(int, int) = &add; + int result = fp(3, 4); + std::cout << " fp(3, 4) = " << result << "\n"; + } + + std::cout << "\n=== Lambda / 仿函数 ===\n"; + { + auto lam = [](int a, int b) { return a + b; }; + std::cout << " lambda(3, 4) = " << lam(3, 4) << "\n"; + + Adder fn; + std::cout << " functor(3, 4) = " << fn(3, 4) << "\n"; + } + + std::cout << "\n=== 成员函数指针 ===\n"; + { + Calculator calc; + int (Calculator::*pmf)(int, int) = &Calculator::multiply; + int result = (calc.*pmf)(3, 4); + std::cout << " (calc.*multiply)(3, 4) = " << result << "\n"; + } + + std::cout << "\n=== 数据成员指针 ===\n"; + { + Point p{1.0, 2.0}; + double Point::*pmx = &Point::x; + double val = p.*pmx; + std::cout << " p.*&Point::x = " << val << "\n"; + } + + std::cout << "\n=== std::invoke 统一调用 ===\n"; + { + // 成员函数 + 对象引用 + Calculator calc; + int r1 = std::invoke(&Calculator::multiply, calc, 3, 4); + std::cout << " invoke(member_func, ref, 3, 4) = " << r1 << "\n"; + + // 成员函数 + 对象指针 + int r2 = std::invoke(&Calculator::multiply, &calc, 3, 4); + std::cout << " invoke(member_func, ptr, 3, 4) = " << r2 << "\n"; + + // 数据成员 + Point p{5.0, 6.0}; + double r3 = std::invoke(&Point::x, p); + std::cout << " invoke(&Point::x, p) = " << r3 << "\n"; + + // lambda + int r4 = std::invoke([](int a, int b) { return a + b; }, 3, 4); + std::cout << " invoke(lambda, 3, 4) = " << r4 << "\n"; + } + + std::cout << "\n=== std::invoke_result_t 编译期推导 ===\n"; + { + using R1 = std::invoke_result_t; + static_assert(std::is_same_v); + std::cout << " invoke_result_t == int\n"; + + auto lam = [](double x) { return std::to_string(x); }; + using R2 = std::invoke_result_t; + static_assert(std::is_same_v); + std::cout << " invoke_result_t == std::string\n"; + } + + return 0; +} diff --git a/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/09_concepts_requires.cpp b/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/09_concepts_requires.cpp new file mode 100644 index 000000000..fc062545a --- /dev/null +++ b/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/09_concepts_requires.cpp @@ -0,0 +1,82 @@ +// Concepts 与 requires 约束 +// 来源:OnceCallback 前置知识(四)(pre-04) +// 编译:g++ -std=c++20 -Wall -Wextra 09_concepts_requires.cpp -o 09_concepts_requires + +#include +#include +#include +#include + +// --- 自定义 concept --- + +template +concept Integral = std::is_integral_v; + +template + requires Integral +void foo(T x) { + std::cout << " foo(" << x << "): T is integral\n"; +} + +// --- not_the_same_t concept --- + +template +concept not_the_same_t = !std::is_same_v, T>; + +// --- 模板构造函数劫持演示 --- + +struct Wrapper { + Wrapper() = default; + + template + requires (!std::same_as, Wrapper>) + Wrapper(T&&) { + std::cout << " template constructor called\n"; + } + + Wrapper(Wrapper&&) noexcept { + std::cout << " move constructor called\n"; + } +}; + +int main() { + std::cout << "=== 标准库 concepts ===\n"; + { + static_assert(std::invocable); + std::cout << " int(*)(int) is invocable with int: OK\n"; + + static_assert(std::same_as); + std::cout << " same_as: OK\n"; + + static_assert(std::convertible_to); + std::cout << " convertible_to: OK\n"; + } + + std::cout << "\n=== 自定义 concept Integral ===\n"; + { + foo(42); // OK:int 是整数 + // foo(3.14); // 编译错误:double 不满足 Integral + } + + std::cout << "\n=== not_the_same_t concept ===\n"; + { + static_assert(not_the_same_t); + static_assert(!not_the_same_t); + static_assert(!not_the_same_t); // decay_t 去掉引用和 const + std::cout << " not_the_same_t: true\n"; + std::cout << " not_the_same_t: false\n"; + std::cout << " not_the_same_t: false (after decay)\n"; + } + + std::cout << "\n=== 模板构造函数 vs 移动构造函数 ===\n"; + { + Wrapper a; + std::cout << " moving Wrapper:\n"; + Wrapper b = std::move(a); // 应该调用移动构造函数,而非模板构造函数 + + std::cout << " constructing from int:\n"; + Wrapper c(42); // 应该调用模板构造函数 + } + + return 0; +} diff --git a/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/10_move_only_function.cpp b/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/10_move_only_function.cpp new file mode 100644 index 000000000..1fe943efd --- /dev/null +++ b/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/10_move_only_function.cpp @@ -0,0 +1,88 @@ +// std::move_only_function (C++23) +// 来源:OnceCallback 前置知识(五)(pre-05) +// 编译:g++ -std=c++23 -Wall -Wextra 10_move_only_function.cpp -o 10_move_only_function + +#include +#include +#include +#include + +int add(int a, int b) { return a + b; } + +struct Multiplier { + int operator()(int a, int b) { return a * b; } +}; + +int main() { + std::cout << "=== std::function 不支持 move-only ===\n"; + { + // auto ptr = std::make_unique(42); + // std::function f = [p = std::move(ptr)]() { return *p; }; + // 编译错误!unique_ptr 不可拷贝,std::function 要求可拷贝 + std::cout << " std::function cannot hold move-only callables\n"; + } + + std::cout << "\n=== std::move_only_function 支持 move-only ===\n"; + { + auto ptr = std::make_unique(42); + std::move_only_function f = [p = std::move(ptr)]() { return *p; }; + int result = f(); + std::cout << " move_only_function with unique_ptr: " << result << "\n"; + } + + std::cout << "\n=== 构造方式 ===\n"; + { + // 从 lambda 构造 + std::move_only_function f1 = [](int a, int b) { return a + b; }; + + // 从函数指针构造 + std::move_only_function f2 = &add; + + // 从仿函数构造 + std::move_only_function f3 = Multiplier{}; + + // 默认构造:空的 + std::move_only_function f4; + + std::cout << " f1(3, 4) = " << f1(3, 4) << "\n"; + std::cout << " f2(3, 4) = " << f2(3, 4) << "\n"; + std::cout << " f3(3, 4) = " << f3(3, 4) << "\n"; + std::cout << " f4 is " << (f4 ? "not null" : "null") << "\n"; + } + + std::cout << "\n=== 判空与清空 ===\n"; + { + std::move_only_function f; + if (!f) { + std::cout << " default-constructed is null\n"; + } + + f = []() { return 42; }; + if (f) { + std::cout << " after assignment: not null, f() = " << f() << "\n"; + } + + f = nullptr; + if (!f) { + std::cout << " after nullptr: null again\n"; + } + } + + std::cout << "\n=== 移动后状态未指定 ===\n"; + { + std::move_only_function f = []() { return 42; }; + auto g = std::move(f); + std::cout << " g() = " << g() << "\n"; + std::cout << " f after move: " << (f ? "not null" : "null") << " (unspecified)\n"; + } + + std::cout << "\n=== sizeof 对比 ===\n"; + { + std::cout << " sizeof(std::function): " + << sizeof(std::function) << " bytes\n"; + std::cout << " sizeof(std::move_only_function): " + << sizeof(std::move_only_function) << " bytes\n"; + } + + return 0; +} diff --git a/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/11_deducing_this.cpp b/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/11_deducing_this.cpp new file mode 100644 index 000000000..f8042d1ea --- /dev/null +++ b/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/11_deducing_this.cpp @@ -0,0 +1,86 @@ +// Deducing this / 显式对象参数 (C++23) +// 来源:OnceCallback 前置知识(六)(pre-06) +// 编译:g++ -std=c++23 -Wall -Wextra 11_deducing_this.cpp -o 11_deducing_this + +#include +#include +#include + +// --- 基本推导规则验证 --- + +struct Check { + void test(this auto&& self) { + using Self = decltype(self); + if constexpr (std::is_lvalue_reference_v) { + if constexpr (std::is_const_v>) { + std::cout << " const lvalue reference\n"; + } else { + std::cout << " lvalue reference\n"; + } + } else { + std::cout << " rvalue (not a reference)\n"; + } + } +}; + +// --- 模拟 OnceCallback::run() 的左值拦截 --- + +struct SimpleCallback { + void run(this auto&& self) { + static_assert(!std::is_lvalue_reference_v, + "SimpleCallback::run() must be called on an rvalue. " + "Use std::move(cb).run() instead."); + std::cout << " callback executed!\n"; + } +}; + +// --- 常量传播与 const 方法区分 --- + +struct Value { + int v = 0; + + void get(this const auto& self) { + std::cout << " const access: " << self.v << "\n"; + } + + void mutate(this auto&& self) { + if constexpr (!std::is_const_v>) { + self.v++; + std::cout << " mutated to: " << self.v << "\n"; + } else { + std::cout << " cannot mutate const object\n"; + } + } +}; + +int main() { + std::cout << "=== deducing this 推导规则 ===\n"; + { + Check c; + c.test(); // lvalue reference + std::move(c).test(); // rvalue + std::as_const(c).test(); // const lvalue reference + } + + std::cout << "\n=== 模拟左值拦截 ===\n"; + { + SimpleCallback cb; + // cb.run(); // 编译错误!左值调用被拦截 + std::move(cb).run(); // OK:右值调用 + SimpleCallback().run(); // OK:临时对象也是右值 + } + + std::cout << "\n=== const/mutate 区分 ===\n"; + { + Value v{10}; + v.get(); // const access + v.mutate(); // mutated to 11 + v.mutate(); // mutated to 12 + + const Value cv{99}; + cv.get(); // const access + // cv.mutate(); // prints "cannot mutate const object" + } + + return 0; +} diff --git a/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/CMakeLists.txt b/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/CMakeLists.txt new file mode 100644 index 000000000..a29cbeb1d --- /dev/null +++ b/code/volumn_codes/vol9/full_tutorial_codes/chrome_design/CMakeLists.txt @@ -0,0 +1,56 @@ +cmake_minimum_required(VERSION 3.20) +project(chrome_design_prerequisites LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# C++17 示例(无需 C++20/23 特性) +set(CPP17_EXAMPLES + 01_move_semantics + 02_smart_pointers + 03_atomic_memory_order + 04_lambda_basics + 06_type_traits + 07_function_type_specialization + 08_invoke +) + +# C++20 示例(需要 concepts / lambda pack expansion) +set(CPP20_EXAMPLES + 05_lambda_advanced + 09_concepts_requires +) + +# C++23 示例(需要 move_only_function / deducing this) +set(CPP23_EXAMPLES + 10_move_only_function + 11_deducing_this +) + +# C++17 targets +foreach(name ${CPP17_EXAMPLES}) + add_executable(${name} ${name}.cpp) + target_compile_options(${name} PRIVATE -Wall -Wextra -Wpedantic) + set_target_properties(${name} PROPERTIES CXX_STANDARD 17) +endforeach() + +# C++20 targets +foreach(name ${CPP20_EXAMPLES}) + add_executable(${name} ${name}.cpp) + target_compile_options(${name} PRIVATE -Wall -Wextra -Wpedantic) + set_target_properties(${name} PROPERTIES CXX_STANDARD 20) +endforeach() + +# C++23 targets +foreach(name ${CPP23_EXAMPLES}) + add_executable(${name} ${name}.cpp) + target_compile_options(${name} PRIVATE -Wall -Wextra -Wpedantic) + set_target_properties(${name} PROPERTIES CXX_STANDARD 23) +endforeach() + +# threading support for atomic example +find_package(Threads REQUIRED) +target_link_libraries(03_atomic_memory_order PRIVATE Threads::Threads) diff --git a/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/01-1-once-callback-motivation-and-api-design.md b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/01-1-once-callback-motivation-and-api-design.md new file mode 100644 index 000000000..c54ce828f --- /dev/null +++ b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/01-1-once-callback-motivation-and-api-design.md @@ -0,0 +1,257 @@ +--- +title: "OnceCallback 实战(一):动机与接口设计" +description: "从一次真实的异步回调 bug 出发,拆解 std::function 在异步场景的三大缺陷,设计 OnceCallback 的完整目标 API" +chapter: 1 +order: 1 +tags: + - host + - cpp-modern + - beginner + - 回调机制 + - 函数对象 +difficulty: beginner +platform: host +cpp_standard: [23] +reading_time_minutes: 11 +prerequisites: + - "OnceCallback 前置知识(一):函数类型与模板偏特化" + - "OnceCallback 前置知识(五):std::move_only_function" + - "OnceCallback 前置知识(六):Deducing this" +related: + - "OnceCallback 实战(二):核心骨架搭建" + - "OnceCallback 前置知识速查:C++11/14/17 核心特性回顾" +--- + +# OnceCallback 实战(一):动机与接口设计 + +## 引言 + +说实话,笔者在做异步编程的时候,踩过最多的坑就是回调被多次调用。场景很经典——注册一个文件 I/O 完成的回调,期望它跑一次就完事,结果因为某处逻辑手滑多触发了一次,回调里释放的资源被二次访问,直接喜提段错误。这种 bug 的一大特点是——在测试里很难复现,因为正常的异步路径往往只跑一次回调;真正的触发条件是某种竞态或错误重试路径。 + +`std::function` 没法帮我们。它允许多次调用,允许拷贝传播,回调对象可以满天飞。我们需要的是一种**在类型系统层面就约束住回调语义**的机制——让"只能调用一次"这个规则变成编译器的检查项,而不是程序员记忆力的事。 + +这一篇我们从动机出发,拆清楚 `std::function` 到底哪里不对,然后设计我们的目标 API。下一篇再开始写代码。 + +> **学习目标** +> +> - 从一个真实的异步 bug 理解 `std::function` 在回调场景的三大缺陷 +> - 掌握 Chromium OnceCallback 的设计哲学:move-only + 右值限定 + 单次消费 +> - 设计出 OnceCallback 的完整公共接口 + +--- + +## 从一个 bug 说起 + +### 场景:异步文件读取 + +假设我们在写一个异步文件读取的封装。用户调用 `read_file_async(path, callback)`,I/O 完成后 `callback` 被触发一次,传入文件内容。 + +```cpp +void read_file_async(const std::string& path, + std::function callback); + +// 使用 +void on_file_read(std::string content) { + process(content); // 处理内容 + release_resources(); // 释放相关资源 +} + +read_file_async("data.txt", on_file_read); +``` + +看起来没问题。但如果 I/O 子系统因为某种错误触发了重试——回调被调用了两次。`release_resources()` 执行了两次,第二次访问的是已释放的内存。段错误。在测试环境中,这个重试路径永远不会被触发;只有在生产环境的高并发场景下,这个 bug 才会以极低的概率出现。 + +### std::function 没有帮我们 + +问题出在哪里?`std::function` 的类型签名里没有任何信息告诉我们"这个回调应该被调用几次"。类型系统没有提供约束,只能靠运行时的断言——如果你有的话——或者靠程序员的纪律来保证。 + +更糟糕的是,`std::function` 的特性让这个问题变得更难发现。它是可拷贝的,意味着回调可以被复制到多个地方。如果多个执行路径同时持有同一份回调的副本,竞态条件就埋伏在其中。它的 `operator()` 是 `const` 限定的——调用它不会改变 `std::function` 对象本身的状态,所以你无法通过调用接口来表达"调用即消费"这个语义。 + +--- + +## std::function 的三大缺陷 + +我们把问题系统化一下。`std::function` 作为通用的可调用对象容器,在设计上是成功的——但在异步回调这个特定场景下,它有三个致命的问题。 + +### 缺陷一:可复制 + +`std::function` 天生支持拷贝。当你拷贝一个 `std::function` 时,它内部的类型擦除机制会把存储的可调用对象也拷贝一份。在异步系统中,这意味着一个回调可以被复制到任意多个地方——任务队列里一份、定时器里一份、错误处理器里一份——每份都可以独立调用。 + +如果回调里捕获了 move-only 的资源(比如 `std::unique_ptr`),拷贝直接编译失败。如果捕获的是裸指针或引用,多个副本同时执行就会产生竞态。Chrome 团队的思路很直接:既然异步任务回调从根本上就不应该被复制,那就让它在类型层面不可拷贝。 + +### 缺陷二:可重复调用 + +`std::function::operator()` 对调用次数没有任何约束。你可以在同一个 `std::function` 上调一千次,它照跑不误。但在异步回调场景里,一个文件读取完成的回调被调用两次就是逻辑错误——它可能触发两次资源释放、两次状态转换、两次消息发送。这种错误在类型系统里完全检测不到。 + +### 缺陷三:无法表达消费语义 + +在 Chrome 的任务投递模型中,一个 `PostTask(FROM_HERE, callback)` 调用之后,`callback` 就不应该再被使用——它的所有权已经转移给了任务系统。`std::function` 的 `operator()` 是 `const` 限定的,调用它不会改变 `std::function` 对象本身的状态,所以你无法通过调用接口来表达"调用即消费"这个语义。 + +这三个问题归结到一点:`std::function` 的接口设计无法表达"这个回调只能被调用一次,调用后即失效"这个约束。我们的 OnceCallback 就是为了填补这个语义空白而设计的。 + +--- + +## Chromium 的回答:OnceCallback 设计哲学 + +Chrome 的回调系统建立在一条核心原则之上:**消息传递优于锁,序列化优于线程**。在这个原则下,每个投递到任务系统的回调都是一个独立的、一次性的消息。投递之后,回调的所有权就从调用方转移到了任务系统;执行之后,回调就被销毁。没有共享,没有复用,没有歧义。 + +这个哲学直接体现在 `OnceCallback` 的类型设计上,三个关键约束: + +**Move-only**:`OnceCallback` 删除了拷贝构造和拷贝赋值,只保留移动操作。从类型层面保证回调在任意时刻只有一个持有者。 + +**右值限定 Run()**:`OnceCallback::Run()` 只能通过右值引用调用。左值调用触发编译错误。从语法层面提醒调用方:"你在消费这个回调,之后别再用了。" + +**单次消费**:`Run()` 内部会通过引用计数机制销毁 `BindState`,使得后续对同一对象的任何访问都是安全的空操作。 + +### Chromium 内部架构概览 + +Chromium 的回调系统由三个层次组成。底层是 `BindStateBase`——类型擦除的基类,带引用计数,不用虚函数而是用函数指针成员来实现多态。中间层是 `BindState`——模板化的具体类,存储真正的可调用对象和绑定参数。顶层是 `OnceCallback`——用户直接操作的类型,本质上是 `BindState` 的一个智能指针包装,大小只有 8 字节。 + +我们的实现会保留"外层接口 + 内部存储 + 类型擦除"的分层思路,但用 `std::move_only_function` 来替代 Chromium 手写的 `BindState` + 引用计数组合,用 deducing this 来替代双重重载 + `!sizeof` hack。 + +--- + +## 设计目标 API + +我们把目标 API 定下来,再回头讨论每个设计决策。这是工程师的工作方式——先想清楚"我要什么",再想"怎么做"。 + +### 构造与调用 + +```cpp +#include "once_callback/once_callback.hpp" + +using namespace tamcpp::chrome; + +// 从 lambda 构造 +auto cb = OnceCallback([](int a, int b) { + return a + b; +}); + +// 调用:必须通过右值 +int result = std::move(cb).run(3, 4); // result == 7 + +// 调用后 cb 被消费 +// std::move(cb).run(1, 2); // 运行时断言失败 +``` + +### 参数绑定 + +```cpp +// bind_once:预绑定部分参数,返回一个新的 OnceCallback +auto bound = bind_once( + [](int x, int y, int z) { return x + y + z; }, + 10, 20 // 预绑定前两个参数 +); + +int r = std::move(bound).run(30); // r == 60 +``` + +### 取消检查 + +```cpp +auto cb = OnceCallback([](int x) { /* ... */ }); + +// 检查回调是否仍然有效 +if (!cb.is_cancelled()) { + std::move(cb).run(42); +} + +// maybe_valid:乐观检查 +if (cb.maybe_valid()) { + std::move(cb).run(42); +} +``` + +### 链式组合 + +```cpp +auto pipeline = OnceCallback([](int a, int b) { + return a + b; +}).then([](int sum) { + return sum * 2; +}); + +int final_result = std::move(pipeline).run(3, 4); +// final_result == 14,因为 (3+4)*2 = 14 +``` + +--- + +## 接口设计决策分析 + +### 为什么用 run() 而不是 operator() + +Chromium 用的是 `Run()`(Google 风格要求大写开头)。我们用 `run()` 符合 snake_case 命名规范。更深层的原因是语义区分——`operator()` 太通用,任何可调用对象都有 `operator()`;`run()` 明确表达了"执行任务"的语义,在代码审查时一眼就能看出这是在消费一个 OnceCallback,而不是调用一个普通函数。 + +### 为什么 run() 必须通过右值 + +这是整个设计中最关键的一点。我们用 deducing this 让编译器帮我们拦截左值调用——如果写 `cb.run(args)` 而不是 `std::move(cb).run(args)`,编译器直接报错,错误信息明确告诉你该怎么做。这个机制在前置知识(六)里已经详细讲过了。 + +### 为什么区分 is_cancelled() 和 maybe_valid() + +区别在于安全保证的强弱。`is_cancelled()` 提供确定性回答——只能在回调绑定的序列上调用,保证返回准确的结果。`maybe_valid()` 提供乐观估计——可以从任何线程调用,但结果可能过时。在 Chromium 的完整实现中,这个区分和线程安全保证有关。我们的简化版暂时让两者语义相同,但保留了接口以备后续扩展。 + +### 为什么 then() 消费 *this + +`then()` 的语义是"把当前回调的执行结果传给下一个回调"。这要求当前回调在 `then()` 返回的新回调中被完整捕获。如果 `then()` 不消费 `*this`,同一个回调就会同时存在于两个地方——违反 move-only 的语义约束。所以 `then()` 被声明为右值限定成员函数,调用后原回调对象进入已消费状态。 + +--- + +## 环境搭建 + +开始写代码之前,确认一下工具链。OnceCallback 依赖 `std::move_only_function` 和 deducing this,都是 C++23 特性。 + +### 编译器要求 + +GCC 13+ 或 Clang 17+ 可以完整支持上述特性。编译时加 `-std=c++23`。 + +### 验证代码 + +```cpp +#include + +// 验证 std::move_only_function 可用 +static_assert(__cpp_lib_move_only_function >= 202110L); + +// 验证 deducing this 可用 +struct Check { + void test(this auto&& self) {} +}; + +int main() { + Check c; + c.test(); + return 0; +} +``` + +如果这段代码编译通过,环境就绑了。 + +### CMake 最小配置 + +```cmake +cmake_minimum_required(VERSION 3.20) +project(once_callback_demo LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_library(once_callback INTERFACE) +target_include_directories(once_callback INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/.. +) +``` + +--- + +## 小结 + +这一篇我们从动机出发,搞清楚了三件事。`std::function` 在异步回调场景有三大缺陷——可复制、可重复调用、无法表达消费语义——根源在于类型系统无法约束"只能调用一次"。Chromium 的 OnceCallback 通过 move-only + 右值限定 Run() + 单次消费来填补这个语义空白。我们设计了一套目标 API,包括构造与调用、参数绑定(`bind_once`)、取消检查(`is_cancelled`/`maybe_valid`)和链式组合(`then()`)四个核心功能。 + +下一篇我们开始搭建核心骨架——从模板偏特化到三态管理,把 OnceCallback 的类骨架搭起来。 + +## 参考资源 + +- [Chromium Callback 文档](https://chromium.googlesource.com/chromium/src/+/main/docs/callback.md) +- [cppreference: std::move_only_function](https://en.cppreference.com/w/cpp/utility/functional/move_only_function) +- [P0847R7 - Deducing this 提案](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0847r7.html) diff --git a/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/01-2-once-callback-core-skeleton.md b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/01-2-once-callback-core-skeleton.md new file mode 100644 index 000000000..af36c32c8 --- /dev/null +++ b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/01-2-once-callback-core-skeleton.md @@ -0,0 +1,309 @@ +--- +title: "OnceCallback 实战(二):核心骨架搭建" +description: "从零开始五步搭建 OnceCallback 的类骨架——模板偏特化、数据成员、构造函数约束、run() 消费语义、查询接口" +chapter: 1 +order: 2 +tags: + - host + - cpp-modern + - beginner + - 回调机制 + - 函数对象 + - 模板 +difficulty: beginner +platform: host +cpp_standard: [23] +reading_time_minutes: 13 +prerequisites: + - "OnceCallback 实战(一):动机与接口设计" + - "OnceCallback 前置知识(一):函数类型与模板偏特化" + - "OnceCallback 前置知识(四):Concepts 与 requires 约束" + - "OnceCallback 前置知识(五):std::move_only_function" + - "OnceCallback 前置知识(六):Deducing this" +related: + - "OnceCallback 实战(三):bind_once 实现" + - "OnceCallback 实战(四):取消令牌设计" +--- + +# OnceCallback 实战(二):核心骨架搭建 + +## 引言 + +上一篇我们搞清楚了"为什么需要 OnceCallback"和"目标 API 长什么样"。现在我们正式上手写代码。这一篇的任务是把 OnceCallback 的类骨架从零搭建起来——不是一口气写完所有功能,而是分五步,每一步在前一步的基础上加一层。搭完骨架之后,后续的 `bind_once`、取消令牌、`then()` 都是往这个骨架上加组件。 + +所有前置知识我们在前面七篇文章里都已经讲透了。这一篇是纯实战——我们直接对照实际源码,把每一个设计决策落实到代码上。 + +> **学习目标** +> +> - 从零搭建 `OnceCallback` 的完整类骨架 +> - 理解每个数据成员和方法的职责 +> - 掌握 `run()` 的 deducing this 实现和 `impl_run()` 的消费逻辑 + +--- + +## 第一步:主模板与偏特化 + +前置知识(一)里我们已经讲过"函数类型 + 模板偏特化"这个模式。现在把它直接应用到 OnceCallback 上。 + +```cpp +namespace tamcpp::chrome { + +// 主模板:只有声明,没有定义 +// 如果有人写了 OnceCallback(传了非函数类型),编译器会报错 +template +class OnceCallback; + +// 偏特化:FuncSignature 是 R(Args...) 形式的函数类型时匹配 +template +class OnceCallback { + // 所有真正的代码都在这个偏特化里 +public: + using FuncSig = ReturnType(FuncArgs...); + // ... +}; + +} // namespace tamcpp::chrome +``` + +当你写 `OnceCallback` 时,编译器把 `int(int, int)` 匹配到主模板的 `FuncSignature`,然后发现偏特化能把它拆成 `ReturnType = int`、`FuncArgs = {int, int}`,于是选择偏特化版本。`FuncSig` 是一个类型别名,保存了完整的函数签名——后面声明 `std::move_only_function` 时会用到。 + +--- + +## 第二步:数据成员——三个核心存储 + +现在往偏特化类里添加数据成员。OnceCallback 需要三个东西来管理自己的状态。 + +```cpp +template +class OnceCallback { +public: + using FuncSig = ReturnType(FuncArgs...); + +private: + enum class Status : uint8_t { + kEmpty, // 从未被赋值(默认构造) + kValid, // 持有有效的可调用对象 + kConsumed // 已被 run() 调用过 + } status_ = Status::kEmpty; + + std::move_only_function func_; // 类型擦除的可调用对象 + std::shared_ptr token_; // 可选的取消令牌 +}; +``` + +`func_` 是类型擦除的核心——它把各种不同形态的可调用对象(lambda、函数指针、仿函数)统一包装成 `FuncSig` 签名的调用接口。不管你传入什么,`func_` 都能用同一个 `operator()` 调用它。 + +`status_` 是一个三态枚举,区分"从未赋值"、"随时可调用"和"已经调用过了"。为什么不能只靠 `func_` 的判空?因为 `std::move_only_function` 的 `operator bool()` 只能区分"空"和"非空"两种状态,而且移动后的状态未指定——前置知识(五)里已经详细讲过了。 + +`token_` 是一个可选的取消令牌,用于在回调执行前检查是否应该取消执行。默认是空指针(不启用取消机制),通过 `set_token()` 方法设置。这个我们后面有专门一篇讲。 + +--- + +## 第三步:构造函数与 requires 约束 + +接下来添加构造函数。这里的关键点是模板构造函数必须用 `requires` 约束来防止它劫持移动构造函数——前置知识(四)里已经讲过这个问题了。 + +```cpp +// not_the_same_t concept:F 退化后不是 T +template +concept not_the_same_t = !std::is_same_v, T>; + +template +class OnceCallback { + // ... 数据成员 ... + + // 禁止拷贝 + OnceCallback(const OnceCallback&) = delete; + OnceCallback& operator=(const OnceCallback&) = delete; + +public: + // 模板构造函数:接受任意可调用对象 + template + requires not_the_same_t + explicit OnceCallback(Functor&& function) + : status_(Status::kValid), func_(std::move(function)) {} + + // 默认构造:创建空回调 + explicit OnceCallback() = default; + + // 移动构造 + OnceCallback(OnceCallback&& other) noexcept + : status_(other.status_), + func_(std::move(other.func_)), + token_(std::move(other.token_)) { + other.status_ = Status::kEmpty; + } + + // 移动赋值 + OnceCallback& operator=(OnceCallback&& other) noexcept { + if (this != &other) { + status_ = other.status_; + func_ = std::move(other.func_); + token_ = std::move(other.token_); + other.status_ = Status::kEmpty; + } + return *this; + } +}; +``` + +让我们逐个理解这些构造函数。 + +**模板构造函数**是最常用的——当你写 `OnceCallback([](int x) { return x; })` 时调用的就是这个。`Functor` 被推导为 lambda 的闭包类型,`requires not_the_same_t` 确保当传入的是 `OnceCallback` 本身时模板被排除(让移动构造函数来处理)。`std::move(function)` 把传入的可调用对象移入 `func_`,`status_` 设为 `kValid`。 + +**默认构造函数**创建一个空的 OnceCallback——`status_` 是 `kEmpty`(由成员初始化器的默认值决定),`func_` 和 `token_` 都是空的。 + +**移动构造函数**从另一个 OnceCallback 那里偷走所有内容——`func_` 和 `token_` 通过 `std::move` 转移,`status_` 也一起复制过来。关键点是移动后源对象被设为 `kEmpty`——这是我们主动做的,不是依赖 `std::move_only_function` 的移动后状态。 + +--- + +## 第四步:run() 的 deducing this 实现 + +这一步是整个骨架的灵魂。`run()` 利用 deducing this 在编译期拦截左值调用,通过右值调用时转发到内部的 `impl_run()`。 + +```cpp +// 声明(在类体内) +template +auto run(this Self&& self, FuncArgs&&... args) -> ReturnType; + +// 实现(在类体外,once_callback_impl.hpp 中) +template +template +auto OnceCallback::run(this Self&& self, FuncArgs&&... args) + -> ReturnType { + static_assert(!std::is_lvalue_reference_v, + "once_callback::run() must be called on an rvalue. " + "Use std::move(cb).run(...) instead."); + return std::forward(self).impl_run(std::forward(args)...); +} +``` + +当调用方写 `cb.run(args)` 时,`Self` 被推导为 `OnceCallback&`(左值引用),`static_assert` 触发,报错信息直接告诉调用方该怎么做。当写 `std::move(cb).run(args)` 时,`Self` 被推导为 `OnceCallback`(非引用),编译通过,转发到 `impl_run`。 + +`impl_run` 是真正执行回调的地方: + +```cpp +template +ReturnType OnceCallback::impl_run(FuncArgs... args) { + assert(status_ == Status::kValid); + + // 取消检查:消费但不执行 + if (token_ && !token_->is_valid()) { + status_ = Status::kConsumed; + func_ = nullptr; + if constexpr (std::is_void_v) { + return; + } else { + throw std::bad_function_call{}; + } + } + + // 消费:先把 func_ 拿出来,再更新状态,最后执行 + auto functor = std::move(func_); + func_ = nullptr; + status_ = Status::kConsumed; + + if constexpr (std::is_void_v) { + functor(std::forward(args)...); + } else { + return functor(std::forward(args)...); + } +} +``` + +有几个关键细节值得注意。 + +先看消费顺序——`impl_run` 先把 `func_` move 出来作为局部变量 `functor`,然后把 `func_` 置空、`status_` 设为 kConsumed,最后执行 `functor`。这个顺序很重要:先把可调用对象拿出去、状态标记好,再执行。即使可调用对象内部抛出异常,`status_` 也已经是 `kConsumed` 了,回调不会处于不一致的状态。 + +再看 `if constexpr`——void 返回类型不能用常规方式赋值和返回。`if constexpr (std::is_void_v)` 在编译期选择分支,void 的情况走"调用但不赋值"的路径,非 void 的情况走"调用并赋值给 return"的路径。这是我们速查篇里讲过的标准模式。 + +最后看取消检查——在执行前检查取消令牌。如果已取消,直接消费回调但不执行。void 返回直接 `return`,非 void 返回抛出 `std::bad_function_call`。非 void 的抛异常行为可能看起来激进,但理由很充分:调用方期望得到一个返回值,但我们无法提供一个有意义的值,所以抛异常比返回未定义值更安全。 + +--- + +## 第五步:查询接口 + +最后加上一组查询方法,让调用方可以在执行前检查回调的状态。 + +```cpp +[[nodiscard]] bool is_cancelled() const noexcept { + if (status_ != Status::kValid) return true; + if (token_ && !token_->is_valid()) return true; + return false; +} + +[[nodiscard]] bool maybe_valid() const noexcept { + return !is_cancelled(); +} + +[[nodiscard]] bool is_null() const noexcept { + return status_ == Status::kEmpty; +} + +explicit operator bool() const noexcept { + return !is_null() && !is_cancelled(); +} + +void set_token(std::shared_ptr token) { + token_ = std::move(token); +} +``` + +`is_cancelled()` 的逻辑是:状态不是 kValid 就返回 true(空回调和已消费回调都算"已取消"),如果有令牌且令牌失效也返回 true。`maybe_valid()` 暂时就是 `!is_cancelled()`。`is_null()` 只检查是否从未被赋值。`operator bool()` 综合了空和取消两个条件。 + +所有查询方法都标注了 `[[nodiscard]]`——调用这些方法就是为了拿返回值做判断,忽略返回值的调用大概率是手滑写错了。`explicit` 关键字防止隐式转换到 `bool`。 + +--- + +## 验证核心骨架 + +骨架搭完了,我们来快速验证几个基本场景: + +```cpp +#include "once_callback/once_callback.hpp" +#include +#include + +int main() { + using namespace tamcpp::chrome; + + // 1. 非 void 返回 + OnceCallback add([](int a, int b) { return a + b; }); + assert(std::move(add).run(3, 4) == 7); + + // 2. void 返回 + bool called = false; + OnceCallback side_effect([&called] { called = true; }); + std::move(side_effect).run(); + assert(called); + + // 3. move-only 捕获 + auto ptr = std::make_unique(42); + OnceCallback capture_move([p = std::move(ptr)] { return *p; }); + assert(std::move(capture_move).run() == 42); + + // 4. 移动语义 + OnceCallback movable([] { return 1; }); + OnceCallback moved_to = std::move(movable); + assert(movable.is_null()); // 源对象变空 + assert(std::move(moved_to).run() == 1); // 目标对象有效 + + return 0; +} +``` + +如果这四个场景都通过——构造回调能拿到正确的返回值、void 回调能正常执行、捕获 `unique_ptr` 的回调用完之后资源被释放、移动后源对象变空目标对象有效——骨架就没有问题。 + +--- + +## 小结 + +这一篇我们分五步搭建了 OnceCallback 的核心骨架。模板偏特化 `OnceCallback` 通过模式匹配拆解函数类型。三个数据成员各司其职——`func_` 负责类型擦除、`status_` 负责三态管理、`token_` 负责取消机制。构造函数用 `requires not_the_same_t` 保护移动构造函数不被劫持。`run()` 用 deducing this 在编译期拦截左值调用,`impl_run()` 通过"先 move 出 func_ 再执行"的顺序保证消费语义的异常安全。 + +下一篇我们往骨架上加第一个组件——`bind_once()`,实现参数绑定。 + +## 参考资源 + +- [Chromium callback.h 源码](https://chromium.googlesource.com/chromium/src/+/HEAD/base/functional/callback.h) +- [cppreference: std::move_only_function](https://en.cppreference.com/w/cpp/utility/functional/move_only_function) +- [P0847R7 - Deducing this 提案](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0847r7.html) diff --git a/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/01-3-once-callback-bind-once.md b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/01-3-once-callback-bind-once.md new file mode 100644 index 000000000..f796fc8e3 --- /dev/null +++ b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/01-3-once-callback-bind-once.md @@ -0,0 +1,213 @@ +--- +title: "OnceCallback 实战(三):bind_once 实现" +description: "逐行拆解 bind_once 的参数绑定实现——从动机到 lambda 捕获包展开,再到手动展开一个完整的模板实例化例子" +chapter: 1 +order: 3 +tags: + - host + - cpp-modern + - beginner + - 回调机制 + - 函数对象 + - 模板 +difficulty: beginner +platform: host +cpp_standard: [23] +reading_time_minutes: 9 +prerequisites: + - "OnceCallback 实战(二):核心骨架搭建" + - "OnceCallback 前置知识(二):std::invoke 与统一调用协议" + - "OnceCallback 前置知识(三):Lambda 高级特性" +related: + - "OnceCallback 实战(四):取消令牌设计" +--- + +# OnceCallback 实战(三):bind_once 实现 + +## 引言 + +核心骨架搭好了,`run()` 能消费回调了。但每次构造 OnceCallback 都得传一个签名叫 `R(Args...)` 的可调用对象,所有参数都得在调用时才传入。现实中经常遇到的情况是:某些参数在创建回调时就已经知道了,只有一部分参数要留到调用时才传入。`bind_once` 就是用来解决这个问题的——它把"已知参数"提前塞进回调里,让调用方只需关心"未知参数"。 + +这一篇我们逐行拆解 `bind_once` 的实现,手动展开一个完整的模板实例化例子,让你看清编译器在背后做了什么。 + +> **学习目标** +> +> - 理解参数绑定解决了什么问题 +> - 逐行理解 `bind_once` 的完整实现 +> - 手动展开一个具体的模板实例化,看清编译器做了什么 +> - 理解为什么 `Signature` 必须显式指定 + +--- + +## 参数绑定解决了什么问题 + +先看一个没有 `bind_once` 时的场景。假设你有一个三参数函数,但前两个参数在绑定时就能确定: + +```cpp +int compute(int x, int y, int z) { + return x + y + z; +} + +// 没有 bind_once:每次调用都得传三个参数 +auto cb = OnceCallback(compute); +int r = std::move(cb).run(10, 20, 30); // r == 60 +``` + +如果 `x = 10` 和 `y = 20` 在绑定时就确定了,只有 `z` 要留到调用时传入,我们希望得到一个只需传一个参数的 `OnceCallback`。 + +不用 `bind_once`,你只能手写一个 lambda 包一层: + +```cpp +auto wrapped = OnceCallback( + [](int z) { return compute(10, 20, z); } +); +int r = std::move(wrapped).run(30); // r == 60 +``` + +能用,但如果参数多了、类型复杂了(比如绑定的是 move-only 的 `unique_ptr`),手写 lambda 就会变得很繁琐。`bind_once` 就是把这个"手写 lambda 包一层"的过程自动化了。 + +```cpp +auto bound = bind_once(compute, 10, 20); +int r = std::move(bound).run(30); // r == 60 +``` + +--- + +## bind_once 的完整实现逐行拆解 + +对照源码,我们逐行理解 `bind_once` 做了什么。 + +```cpp +template +auto bind_once(F&& funtor, BoundArgs&&... args) { + return OnceCallback( + [f = std::forward(funtor), + ...bound = std::forward(args)] + (auto&&... call_args) mutable -> decltype(auto) { + return std::invoke( + std::move(f), + std::move(bound)..., + std::forward(call_args)... + ); + } + ); +} +``` + +### 模板参数 + +`bind_once` 有三个模板参数。`Signature` 是目标回调的函数签名(比如 `int(int)`),必须由调用方显式指定。`F` 是可调用对象的类型(lambda 的闭包类型、函数指针类型等),由编译器从第一个函数参数推导。`BoundArgs...` 是绑定参数的类型包,也是编译器推导的。 + +### lambda 捕获列表 + +捕获列表是整个实现中最精巧的部分。`f = std::forward(funtor)` 用初始化捕获(init capture)把可调用对象完美转发到 lambda 闭包里——如果传入的是右值,它被移动进来;如果传入的是左值,它被拷贝进来。 + +`...bound = std::forward(args)` 是 C++20 引入的 lambda init capture pack expansion。它为 `BoundArgs...` 中的每一个类型生成一个对应的捕获变量,每个变量用 `std::forward` 完美转发初始化。假设 `BoundArgs = {int, std::string}`,展开后等价于: + +```cpp +[f = std::forward(funtor), + b1 = std::forward(arg1), + b2 = std::forward(arg2)] +``` + +### lambda 参数与 mutable + +`(auto&&... call_args)` 是泛型 lambda 的转发引用参数——运行时传入的参数通过它接收。`auto&&` 在这里等效于模板参数的 `T&&`,是转发引用。 + +`mutable` 关键字不可省略——lambda 内部需要调用 `std::move(f)` 和 `std::move(bound)...`,这些操作会修改捕获变量。如果 lambda 是 const 的,捕获变量在内部就是 const 的,没法从 const 对象上 move。 + +### lambda 体 + +```cpp +return std::invoke( + std::move(f), + std::move(bound)..., + std::forward(call_args)... +); +``` + +`std::invoke` 统一处理所有类型的可调用对象——前置知识(二)里已经讲过了。`std::move(f)` 把可调用对象以右值方式传出,`std::move(bound)...` 把所有绑定参数以右值方式传出(因为 `mutable` lambda 内部的捕获变量是左值,需要用 `std::move` 转成右值),`std::forward(call_args)...` 把运行时参数完美转发。 + +绑定参数在前(`std::move(bound)...`),运行时参数在后(`call_args...`),这个顺序很重要——它决定了哪些参数被"预绑定"、哪些参数在调用时才传入。 + +--- + +## 手动展开一个具体例子 + +让我们用一个具体的调用例子,手动展开模板实例化后的完整代码。假设: + +```cpp +struct Calc { + int multiply(int a, int b) { return a * b; } +}; + +Calc calc; +auto bound = bind_once(&Calc::multiply, &calc, 5); +int r = std::move(bound).run(8); // r == 40 +``` + +### 模板参数推导 + +`Signature = int(int)`(显式指定),`F = int (Calc::*)(int, int)`(成员函数指针类型),`BoundArgs = {Calc*, int}`(对象指针 + 第一个参数)。 + +### lambda 捕获展开 + +```cpp +[f = std::forward(&Calc::multiply), + b1 = std::forward(&calc), + b2 = std::forward(5)] +``` + +`f` 捕获了成员函数指针,`b1` 捕获了对象指针,`b2` 捕获了绑定的整数 5。 + +### lambda 体内的 std::invoke 展开 + +当 `bound.run(8)` 被调用时,`call_args = {8}`。`std::invoke` 收到的是: + +```cpp +std::invoke(std::move(f), std::move(b1), std::move(b2), 8) +``` + +也就是: + +```cpp +std::invoke(&Calc::multiply, &calc, 5, 8) +``` + +`std::invoke` 检测到第一个参数是成员函数指针,第二个参数是指向对象的指针,于是展开为: + +```cpp +((*(&calc)).*(&Calc::multiply))(5, 8) +``` + +等价于 `calc.multiply(5, 8)`,结果为 `40`。 + +### 生命周期陷阱 + +注意 `b1 = std::forward(&calc)` 捕获的是一个裸指针 `&calc`。`bind_once` 不会管理 `calc` 的生命周期。如果 `calc` 在回调被调用之前被销毁了,lambda 内部持有的就是一个悬空指针,`std::invoke` 通过悬空指针访问已释放的内存——未定义行为。 + +Chromium 用 `base::Unretained` 显式标记裸指针的安全性,用 `base::Owned` 接管所有权,用 `base::WeakPtr` 在对象析构时自动取消回调。我们的简化版暂时把安全责任交给调用方。 + +--- + +## 为什么签名必须显式指定 + +你可能注意到 `bind_once(...)` 的 `int(int)` 必须手动写。理想情况下,编译器应该能从可调用对象的签名和绑定参数的数量自动推导出剩余签名。但这件事在 C++ 里比想象中困难。 + +对于函数指针 `R(*)(Args...)`,可以通过模板偏特化提取参数列表,然后用编译期的"类型列表切片"去掉前 N 个类型。对于有确定签名的仿函数,可以通过 `decltype(&T::operator())` 提取签名。但对于**泛型 lambda**(`[](auto x) { ... }`),它的 `operator()` 本身是模板,不存在唯一确定的签名——编译器无法在类型层面获取"这个 lambda 接受什么参数"的信息。 + +Chromium 为此写了几百行模板元编程代码来处理各种边界情况。对教学目的来说,让调用方多写一个模板参数 `int(int)` 是更务实的选择。 + +--- + +## 小结 + +这一篇我们逐行拆解了 `bind_once` 的实现。它通过 C++20 的 lambda capture pack expansion 把绑定参数展开到 lambda 的捕获列表中,通过 `std::invoke` 统一处理各种可调用对象(特别是成员函数指针),通过 `mutable` 关键字允许 lambda 内部修改捕获变量。我们手动展开了一个成员函数绑定的完整模板实例化过程,看清了 `std::invoke` 是如何把成员函数指针 + 对象指针展开成普通的成员函数调用的。最后讨论了为什么 `Signature` 必须显式指定——泛型 lambda 的存在让自动推导变得极其复杂。 + +下一篇我们去看取消令牌的设计——一个用 `shared_ptr` 和 `atomic` 实现的轻量级取消机制。 + +## 参考资源 + +- [Chromium bind_internal.h 源码](https://chromium.googlesource.com/chromium/src/+/HEAD/base/functional/bind_internal.h) +- [cppreference: std::invoke](https://en.cppreference.com/w/cpp/utility/functional/invoke) +- [P0780R2 - Pack Expansion in Lambda Capture](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0780r2.html) diff --git a/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/01-4-once-callback-cancellation-token.md b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/01-4-once-callback-cancellation-token.md new file mode 100644 index 000000000..579205d15 --- /dev/null +++ b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/01-4-once-callback-cancellation-token.md @@ -0,0 +1,214 @@ +--- +title: "OnceCallback 实战(四):取消令牌设计" +description: "深入理解 CancelableToken 的设计——用 shared_ptr + atomic 实现轻量级取消机制,以及它如何集成到 OnceCallback 的执行流程中" +chapter: 1 +order: 4 +tags: + - host + - cpp-modern + - beginner + - 回调机制 + - atomic + - 智能指针 + - 引用计数 +difficulty: beginner +platform: host +cpp_standard: [23] +reading_time_minutes: 9 +prerequisites: + - "OnceCallback 实战(二):核心骨架搭建" + - "OnceCallback 前置知识速查:C++11/14/17 核心特性回顾" +related: + - "OnceCallback 实战(五):then 链式组合" + - "OnceCallback 实战(六):测试与性能对比" +--- + +# OnceCallback 实战(四):取消令牌设计 + +## 引言 + +异步编程里有一个很常见的需求:回调创建之后、执行之前,某个外部条件发生了变化,导致这个回调已经没有意义了——比如回调绑定的对象已经被销毁了,或者任务已经被取消了。这时候我们希望回调在执行前能检查一下"我还该不该执行",而不是傻乎乎地跑一遍。 + +这就是取消令牌(cancellation token)的用途。这一篇我们来实现一个简化版的取消令牌,然后看它是怎么集成到 OnceCallback 的执行流程中的。 + +> **学习目标** +> +> - 理解取消令牌的概念和动机 +> - 逐行理解 `CancelableToken` 的实现 +> - 理解取消机制在 `impl_run()` 中的集成方式 +> - 理解 void 和非 void 回调在取消时的不同行为 + +--- + +## 取消令牌的概念 + +你可以把取消令牌想象成一张"通行证"。创建回调的时候,给回调发一张通行证,通行证上写着"有效"。某个时刻外部条件变化了(比如绑定的对象被销毁),外部代码说"通行证作废了"(调用 `invalidate()`)。之后,所有持有这张通行证的回调在执行前检查时都会发现"通行证已经无效",跳过执行。 + +在 Chromium 里,这个"通行证"就是 `WeakPtr` 内部的控制块——`WeakPtr` 指向的对象被销毁后,控制块中的标志位被清除,所有绑定到这个 `WeakPtr` 的回调自动取消。我们的简化版不需要 `WeakPtr` 那么复杂,只需要一个简单的"有效/无效"标志。 + +### 核心需求 + +取消令牌需要满足三个条件:多个回调可以共享同一个令牌(一个 `invalidate()` 让所有回调同时失效)、令牌可以被拷贝和移动(方便在 OnceCallback 内部和外部各持有一份)、失效检查是多线程安全的(外部线程可能在一个线程调用 `invalidate()`,回调在另一个线程检查 `is_valid()`)。 + +--- + +## CancelableToken 的完整实现 + +整个取消令牌只有 18 行代码,但每一行都有它的道理。 + +```cpp +#pragma once +#include +#include + +namespace tamcpp::chrome { +class CancelableToken { + struct Flag { + std::atomic valid{true}; + }; + std::shared_ptr flag_; + +public: + CancelableToken() : flag_(std::make_shared()) {} + + void invalidate() { + flag_->valid.store(false, std::memory_order_release); + } + + bool is_valid() const { + return flag_->valid.load(std::memory_order_acquire); + } +}; +} // namespace tamcpp::chrome +``` + +### 为什么要用嵌套结构体 Flag + +你可能觉得奇怪——为什么不直接在 `CancelableToken` 里放一个 `std::atomic`?原因是 `shared_ptr` 管理的是一个堆上的对象。如果直接在 `CancelableToken` 里放 `atomic`,`shared_ptr` 管理的是 `CancelableToken` 本身——但 `CancelableToken` 还有自己的 `flag_` 成员,这就变成了 `shared_ptr` 包含 `shared_ptr` 的循环。 + +用嵌套的 `Flag` 结构体把需要共享的状态隔离出来,`shared_ptr` 直接管理 `Flag`,`CancelableToken` 的拷贝和移动都通过 `shared_ptr` 的引用计数自动处理——简洁又正确。另一个好处是 `Flag` 结构体方便后续扩展——如果以后需要加更多原子标志(比如取消原因码),直接往 `Flag` 里加就行。 + +### shared_ptr 的共享机制 + +`CancelableToken` 的拷贝构造和拷贝赋值是编译器默认生成的——它做的就是把 `shared_ptr` 拷贝一份,引用计数 +1。所有通过拷贝创建的令牌副本共享同一个 `Flag` 对象。当任何一个副本调用 `invalidate()` 时,修改的是同一个 `Flag::valid`,所有副本在下次调用 `is_valid()` 时都会看到 `false`。 + +```cpp +auto token1 = std::make_shared(); +auto token2 = token1; // 共享同一个 Flag + +token1->invalidate(); +assert(!token2->is_valid()); // token2 也看到了失效 +``` + +### memory_order_acquire/release 配对 + +`invalidate()` 用 `memory_order_release` 存储 `false`,`is_valid()` 用 `memory_order_acquire` 加载。这是一对配对的内存序。`release` store 保证了在 store 之前的所有写操作(包括调用 `invalidate()` 之前的任何状态修改)对其他线程可见。`acquire` load 保证了在 load 之后的所有读操作能看到 release store 之前的写入。 + +在我们的场景里,这意味着如果一个线程调用了 `invalidate()`,另一个线程随后调用 `is_valid()` 时一定能看到 `false`——不会有"我刚刚 invalidate 了但 is_valid 还是返回 true"的情况。这是多线程安全的保证。 + +--- + +## 集成到 OnceCallback + +取消令牌通过 `set_token()` 方法设置到 OnceCallback 中: + +```cpp +void set_token(std::shared_ptr token) { + token_ = std::move(token); +} +``` + +`token_` 是 `shared_ptr` 类型,默认是空指针(不启用取消机制)。设置之后,取消令牌的所有权被转移到 OnceCallback 内部。 + +### is_cancelled() 的完整逻辑 + +```cpp +[[nodiscard]] bool is_cancelled() const noexcept { + if (status_ != Status::kValid) return true; + if (token_ && !token_->is_valid()) return true; + return false; +} +``` + +两层检查。第一层:状态不是 kValid 就返回 true——空回调(kEmpty)和已消费回调(kConsumed)都算"已取消"。这很合理——空回调没东西可执行,已消费回调已经执行过了。第二层:如果有取消令牌且令牌失效了,也返回 true。 + +### impl_run() 中的取消检查 + +```cpp +ReturnType impl_run(FuncArgs... args) { + assert(status_ == Status::kValid); + + // 取消检查在执行前 + if (token_ && !token_->is_valid()) { + status_ = Status::kConsumed; + func_ = nullptr; + if constexpr (std::is_void_v) { + return; + } else { + throw std::bad_function_call{}; + } + } + + // 正常消费流程... +} +``` + +取消检查在执行可调用对象**之前**进行。如果已取消,直接消费回调但不执行——`status_` 设为 kConsumed,`func_` 置为 nullptr(析构其内部的可调用对象,释放资源)。 + +--- + +## void 与非 void 回调的取消行为差异 + +这里有一个设计决策值得展开讲——void 回调被取消时直接 return(不执行,也不报错),而非 void 回调被取消时抛出 `std::bad_function_call` 异常。 + +原因是调用方的期望不同。void 回调的调用方不期望返回值——调用 `std::move(cb).run()` 之后就结束了,不关心回调有没有实际执行。所以被取消的 void 回调直接跳过执行,对调用方是透明的。 + +非 void 回调的调用方期望拿到返回值——`int result = std::move(cb).run()`。如果回调被取消了,我们没法提供一个有意义的返回值。返回一个默认值(比如 0)可能掩盖错误——调用方以为回调正常执行了,实际上什么都没做。抛异常虽然看起来激进,但它明确告诉调用方"出了问题",比默默返回错误值更安全。 + +Chromium 在这里选择直接终止程序(`CHECK` 失败),理由是在 Chrome 的架构中,被取消的回调不应该被调用——调用方应该在调用前检查 `is_cancelled()`。我们选择异常是为了在测试中更容易捕获和验证,而不是直接让程序崩溃。 + +--- + +## 使用示例 + +```cpp +using namespace tamcpp::chrome; + +// 创建令牌和回调 +auto token = std::make_shared(); +bool executed = false; + +OnceCallback cb([&executed] { executed = true; }); +cb.set_token(token); + +// 令牌有效时,正常执行 +assert(!cb.is_cancelled()); +std::move(cb).run(); +assert(executed); // 回调被执行了 + +// 创建另一个回调,这次先取消令牌 +executed = false; +auto cb2 = OnceCallback([&executed] { executed = true; }); +cb2.set_token(token); +token->invalidate(); // 作废令牌 + +assert(cb2.is_cancelled()); +std::move(cb2).run(); // 取消的 void 回调不执行,不抛异常 +assert(!executed); // 回调没有被执行 +``` + +注意第二个例子中——`cb2.run()` 调用了,但回调内部的 lambda 没有执行。`impl_run()` 在执行前检查到令牌已失效,直接消费回调并 return。 + +--- + +## 小结 + +这一篇我们实现了取消令牌并把它集成到了 OnceCallback 中。`CancelableToken` 用 `shared_ptr` + `atomic` 实现了轻量级的取消机制——所有令牌副本共享同一个 `Flag` 对象,一个 `invalidate()` 让所有副本同时失效。集成方式是在 `impl_run()` 执行前检查令牌状态——如果已取消,直接消费回调但不执行。void 回调直接 return,非 void 回调抛出 `std::bad_function_call`,这个差异来自调用方对返回值的不同期望。 + +下一篇我们去看 `then()` 链式组合——OnceCallback 四个功能中所有权设计最精巧的一个。 + +## 参考资源 + +- [cppreference: std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) +- [cppreference: std::atomic](https://en.cppreference.com/w/cpp/atomic/atomic) +- [Chromium WeakPtr 文档](https://chromium.googlesource.com/chromium/src/+/main/docs/memory_model/weak_ptr.md) diff --git a/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/01-5-once-callback-then-chaining.md b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/01-5-once-callback-then-chaining.md new file mode 100644 index 000000000..4dbccab51 --- /dev/null +++ b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/01-5-once-callback-then-chaining.md @@ -0,0 +1,225 @@ +--- +title: "OnceCallback 实战(五):then 链式组合" +description: "逐行拆解 then() 的所有权链设计——从管道思维到 void/非 void 分支处理,理解 OnceCallback 中最精巧的所有权管理" +chapter: 1 +order: 5 +tags: + - host + - cpp-modern + - beginner + - 回调机制 + - 函数对象 + - 模板 +difficulty: beginner +platform: host +cpp_standard: [23] +reading_time_minutes: 9 +prerequisites: + - "OnceCallback 实战(二):核心骨架搭建" + - "OnceCallback 前置知识(二):std::invoke 与统一调用协议" + - "OnceCallback 前置知识(三):Lambda 高级特性" +related: + - "OnceCallback 实战(六):测试与性能对比" +--- + +# OnceCallback 实战(五):then 链式组合 + +## 引言 + +`then()` 允许我们把两个回调串联成一个管道——第一个回调的输出是第二个回调的输入。听起来简单,但它是 OnceCallback 四个功能中所有权设计最精巧的一个。因为 OnceCallback 是 move-only 的,`then()` 必须把原回调的所有权完整地转移到新回调中,不能有任何共享或泄露。 + +这一篇我们从管道思维出发,逐行拆解 `then()` 的实现,重点理解所有权链和 void/非 void 分支的处理。 + +> **学习目标** +> +> - 理解 `then()` 的管道语义和所有权链设计 +> - 逐行理解 `then()` 的完整实现 +> - 理解 void 前缀回调的特殊处理 +> - 对比 `then()` 用 `&&` 限定和 `run()` 用 deducing this 的选择理由 + +--- + +## 管道思维:then() 的语义 + +如果你用过 Unix 管道,`then()` 的语义就很直觉了: + +```bash +# Unix 管道:cmd1 的输出是 cmd2 的输入 +echo "hello" | tr 'h' 'H' | wc -c +``` + +`then()` 做的是同样的事情——回调 A 的输出是回调 B 的输入。用代码表达: + +```cpp +auto pipeline = OnceCallback([](int a, int b) { + return a + b; // 第一步:3 + 4 = 7 +}).then([](int sum) { + return sum * 2; // 第二步:7 * 2 = 14 +}); + +int result = std::move(pipeline).run(3, 4); // result == 14 +``` + +`then()` 把两个独立的回调串联成一个新的回调。调用新回调时,自动走完 A → B 的整个流程。 + +--- + +## 所有权是 then() 的核心挑战 + +串联后的新回调需要持有原回调和后续回调的**所有权**——否则原回调可能在外部被提前消费掉,管道就断了。而 OnceCallback 是 move-only 的,这意味着 `then()` 必须消费 `*this`(原回调)和 `next`(后续回调),把两者的所有权转移到一个新的 lambda 闭包里。 + +整个所有权链条是这样的: + +```text +新 OnceCallback → move_only_function → lambda 闭包 → [原回调 + 后续回调] +``` + +每一层都通过移动语义传递所有权,没有任何共享或拷贝。这就是 move-only 语义在 `then()` 中的完整体现。 + +--- + +## then() 的完整实现逐行拆解 + +```cpp +template +template +auto OnceCallback::then(Next&& next) && { + using NextType = std::decay_t; + + if constexpr (std::is_void_v) { + using NextRet = std::invoke_result_t; + return OnceCallback( + [self = std::move(*this), + cont = std::forward(next)] + (FuncArgs... args) mutable -> NextRet { + std::move(self).run(std::forward(args)...); + return std::invoke(std::move(cont)); + }); + } else { + using NextRet = std::invoke_result_t; + return OnceCallback( + [self = std::move(*this), + cont = std::forward(next)] + (FuncArgs... args) mutable -> NextRet { + auto mid = std::move(self).run(std::forward(args)...); + return std::invoke(std::move(cont), std::move(mid)); + }); + } +} +``` + +### 函数签名:右值限定 + +```cpp +auto then(Next&& next) && +``` + +末尾的 `&&` 使其成为右值限定的成员函数——只能通过 `std::move(cb).then(next)` 或临时对象 `.then(next)` 调用。如果调用方写了 `cb.then(next)`(左值调用),编译器直接报"没有匹配的重载函数"。这是表达消费语义的另一种方式——和 `run()` 用 deducing this 不同,`then()` 不需要区分左值和右值给出不同的错误信息,直接用 ref-qualifier 更简洁。 + +### std::decay_t:退化去掉引用 + +```cpp +using NextType = std::decay_t; +``` + +`Next` 可能是 `SomeLambda&&`(右值引用)或 `SomeLambda&`(左值引用),`std::decay_t` 把引用去掉,得到裸的 lambda 类型。后续用 `NextType` 做类型查询。 + +### if constexpr 的两个分支 + +`then()` 的核心区别在于原回调的返回类型是不是 void。 + +**非 void 分支**:原回调返回一个值,这个值需要传给后续回调。 + +```cpp +using NextRet = std::invoke_result_t; +``` + +`std::invoke_result_t` 在编译期推导"把 `ReturnType` 类型的值传给 `NextType` 类型的可调用对象,返回什么类型"。这就是新回调的返回类型。 + +lambda 内部的执行流程:先调用原回调拿到中间结果 `mid`,再把 `mid` 传给后续回调。 + +```cpp +auto mid = std::move(self).run(std::forward(args)...); +return std::invoke(std::move(cont), std::move(mid)); +``` + +**void 分支**:原回调没有返回值,后续回调不接受参数。 + +```cpp +using NextRet = std::invoke_result_t; +``` + +`std::invoke_result_t` 推导的是"不带参数调用 `NextType`,返回什么类型"。 + +lambda 内部的执行流程:先执行原回调(不拿返回值),再执行后续回调(不传参数)。 + +```cpp +std::move(self).run(std::forward(args)...); +return std::invoke(std::move(cont)); +``` + +### lambda 捕获:所有权的核心 + +```cpp +[self = std::move(*this), cont = std::forward(next)] +``` + +`self = std::move(*this)` 是整个所有权链的关键——它把当前 OnceCallback 对象的**所有内容**(`func_`、`status_`、`token_`)移动到 lambda 的闭包对象里。移动之后,当前对象进入"被移走"的状态——`func_` 和 `token_` 已经被搬走了。 + +`cont = std::forward(next)` 把后续回调也搬进 lambda 闭包。`std::forward` 保持 `next` 的值类别——右值就移动,左值就拷贝。 + +这个 lambda 又被传给一个新的 `OnceCallback` 构造函数,存入新回调的 `std::move_only_function` 里。`move_only_function` 的类型擦除能力保证了不管 lambda 的实际类型是什么,都能被统一存储。 + +--- + +## 多级管道 + +`then()` 可以链式调用,形成多级管道: + +```cpp +using namespace tamcpp::chrome; +auto pipeline = OnceCallback([](int x) { + return x * 2; +}).then([](int x) { + return x + 10; +}).then([](int x) { + return std::to_string(x); +}); + +std::string result = std::move(pipeline).run(5); +// 5 * 2 = 10, 10 + 10 = 20, to_string(20) = "20" +``` + +每次 `then()` 都会创建一个新的 OnceCallback,内部嵌套捕获了前一步的回调。调用最外层的 `run()` 时,执行过程是递归展开的:最外层回调被 `run()` → 执行其 lambda → lambda 内部对上一层调用 `std::move(self).run()` → 再对更上一层调用 → 直到底层。 + +性能上,每一层 `then()` 增加一次 `std::move_only_function` 的间接调用。对于 2-3 级的管道来说完全可接受。如果管道层级超过 10 级,可能需要考虑扁平化的管道结构来避免过深的嵌套——但这已经超出我们当前的讨论范围了。 + +--- + +## 几个容易踩坑的地方 + +### mutable 不可省略 + +lambda 内部需要调用 `std::move(self).run()`——这个操作会修改 `self` 的状态(把 status 从 kValid 改为 kConsumed)。如果 lambda 是 const 的(没加 `mutable`),`self` 在内部就是 const 引用,没法在 const 对象上调用修改状态的操作,编译直接失败。 + +### self = std::move(*this) 的状态 + +移动之后,当前 OnceCallback 对象的 `func_` 和 `token_` 都已经被 move 走了——它们处于"被移走"的状态。`status_` 没有被显式设为 kEmpty,而是保持原来的值。但因为 `func_` 已经被 move 走了,当前对象实际上已经不可用了——任何对它的操作都是未定义的。`then()` 的 `&&` 限定保证了调用方没法在调用 `then()` 之后继续使用原对象。 + +### 为什么用 std::invoke 而不是直接调用 + +`cont` 是一个普通可调用对象(通常是 lambda),直接 `cont(mid)` 也能工作。但 `std::invoke` 是防御性编程——如果有人传进来一个成员函数指针作为后续回调,直接调用语法会失败,`std::invoke` 不会。统一使用 `std::invoke` 保证了无论传什么可调用对象都能正确工作。 + +--- + +## 小结 + +这一篇我们拆解了 `then()` 的完整实现。它的核心挑战是所有权管理——通过 `self = std::move(*this)` 把整个原回调搬进 lambda 闭包,建立完整的所有权链。`if constexpr` 处理 void 和非 void 返回类型的不同语义——void 回调不传参数给后续回调,非 void 回调传递中间结果。`then()` 用 `&&` 限定表达消费语义(比 `run()` 的 deducing this 更简洁,因为不需要自定义错误信息),`mutable` 关键字不可省略(因为内部需要修改 `self` 的状态)。 + +下一篇是系列的最后一篇——我们用系统化的测试用例来验证整个实现,并对比与 Chromium 原版的性能差异。 + +## 参考资源 + +- [Chromium callback.h 源码](https://chromium.googlesource.com/chromium/src/+/HEAD/base/functional/callback.h) +- [cppreference: std::invoke](https://en.cppreference.com/w/cpp/utility/functional/invoke) +- [cppreference: if constexpr](https://en.cppreference.com/w/cpp/language/if) diff --git a/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/01-6-once-callback-testing-and-perf.md b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/01-6-once-callback-testing-and-perf.md new file mode 100644 index 000000000..fa426c9f3 --- /dev/null +++ b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/01-6-once-callback-testing-and-perf.md @@ -0,0 +1,254 @@ +--- +title: "OnceCallback 实战(六):测试与性能对比" +description: "系统化设计六类测试用例验证 OnceCallback 的所有核心行为,对比与 Chromium 原版和标准库方案的性能差异" +chapter: 1 +order: 6 +tags: + - host + - cpp-modern + - beginner + - 回调机制 + - 函数对象 +difficulty: beginner +platform: host +cpp_standard: [23] +reading_time_minutes: 10 +prerequisites: + - "OnceCallback 实战(二):核心骨架搭建" + - "OnceCallback 实战(三):bind_once 实现" + - "OnceCallback 实战(四):取消令牌设计" + - "OnceCallback 实战(五):then 链式组合" +related: + - "OnceCallback 前置知识(五):std::move_only_function" +--- + +# OnceCallback 实战(六):测试与性能对比 + +## 引言 + +到这里,OnceCallback 的四个核心功能——核心骨架、`bind_once`、取消令牌、`then()` 链式组合——都已经实现完了。这一篇做两件事:第一,系统化地梳理测试策略,确保实现在各种边界条件下都是正确的;第二,从性能角度分析我们的实现与 Chromium 原版、标准库方案之间的差异,弄清楚我们牺牲了什么、换来了什么。 + +> **学习目标** +> +> - 掌握按不变量组织测试用例的方法 +> - 理解六类测试的设计意图和关键断言 +> - 清楚我们的 OnceCallback 与 Chromium 原版在性能上的取舍关系 + +--- + +## 测试框架搭建 + +我们使用 Catch2 v3 作为测试框架,通过 CPM(CMake Package Manager)自动拉取依赖。 + +```cmake +# test/CMakeLists.txt +CPMAddPackage("gh:catchorg/Catch2@3.7.1") + +add_executable(test_once_callback test_once_callback.cpp) +target_link_libraries(test_once_callback PRIVATE once_callback Catch2::Catch2WithMain) +target_compile_options(test_once_callback PRIVATE -Wall -Wextra -Wpedantic) + +add_test(NAME test_once_callback COMMAND test_once_callback) +``` + +Catch2 的 `REQUIRE` 宏比 `assert()` 强在它会报告具体的失败表达式、文件和行号,并且在同一个 `TEST_CASE` 内继续执行后续检查。`REQUIRE_THROWS_AS` 专门用于验证异常类型。 + +运行测试:在 `build/` 目录下 `cmake --build . && ctest`。 + +--- + +## 六类测试用例 + +我们把测试组织成六个类别,每个类别聚焦一个独立的设计不变量。按不变量组织测试比按功能组织更不容易遗漏边界情况。 + +### A 类:基本调用与返回值 + +```cpp +TEST_CASE("non-void return", "[once_callback]") { + OnceCallback cb([](int a, int b) { return a + b; }); + int result = std::move(cb).run(3, 4); + REQUIRE(result == 7); +} + +TEST_CASE("void return", "[once_callback]") { + bool called = false; + OnceCallback cb([&called] { called = true; }); + std::move(cb).run(); + REQUIRE(called); +} +``` + +验证最基本的构造和调用行为——非 void 回调返回正确的值,void 回调正常执行。void 返回走的是 `if constexpr (std::is_void_v)` 的另一条分支。 + +### B 类:移动语义 + +```cpp +TEST_CASE("move-only capture", "[once_callback]") { + auto ptr = std::make_unique(42); + OnceCallback cb([p = std::move(ptr)] { return *p; }); + int result = std::move(cb).run(); + REQUIRE(result == 42); +} + +TEST_CASE("move semantics: source becomes null", "[once_callback]") { + OnceCallback cb([] { return 1; }); + OnceCallback cb2 = std::move(cb); + REQUIRE(cb.is_null()); + + int result = std::move(cb2).run(); + REQUIRE(result == 1); +} +``` + +move-only capture 测试验证了 OnceCallback 真正支持 move-only 的可调用对象——如果底层用 `std::function` 而不是 `std::move_only_function`,这段代码编译失败。移动语义测试验证了移动构造后源对象变为 kEmpty 状态。 + +有一个容易搞混的概念点——移动操作转移了所有权,但不会触发消费。只有 `run()` 才会消费回调。`OnceCallback cb2 = std::move(cb1)` 只是转移了所有权,回调在 `cb2.run()` 之前一直处于活跃状态。 + +### C 类:单次调用约束 + +这个约束是通过 deducing this + `static_assert` 实现的——`cb.run()` 会触发编译错误,`std::move(cb).run()` 才能通过。不需要运行时测试,编译通过本身就是验证。 + +### D 类:参数绑定 + +```cpp +TEST_CASE("bind_once basic", "[bind_once]") { + auto bound = bind_once([](int a, int b) { return a * b; }, 5); + int result = std::move(bound).run(8); + REQUIRE(result == 40); +} + +TEST_CASE("bind_once with member function", "[bind_once]") { + struct Calc { + int multiply(int a, int b) { return a * b; } + }; + Calc calc; + auto bound = bind_once(&Calc::multiply, &calc, 5); + int result = std::move(bound).run(8); + REQUIRE(result == 40); +} +``` + +覆盖普通 lambda 的部分参数绑定和成员函数绑定。成员函数绑定的生命周期陷阱在前面的文章里已经讲过了——`&calc` 是裸指针,安全责任在调用方。 + +### E 类:取消机制 + +```cpp +TEST_CASE("is_cancelled respects cancel token", "[once_callback]") { + auto token = std::make_shared(); + OnceCallback cb([] {}); + cb.set_token(token); + + REQUIRE_FALSE(cb.is_cancelled()); + token->invalidate(); + REQUIRE(cb.is_cancelled()); +} + +TEST_CASE("cancelled void callback does not execute", "[once_callback]") { + auto token = std::make_shared(); + bool called = false; + OnceCallback cb([&called] { called = true; }); + cb.set_token(token); + token->invalidate(); + + std::move(cb).run(); + REQUIRE_FALSE(called); +} + +TEST_CASE("cancelled non-void callback throws", "[once_callback]") { + auto token = std::make_shared(); + OnceCallback cb([] { return 1; }); + cb.set_token(token); + token->invalidate(); + + REQUIRE_THROWS_AS(std::move(cb).run(), std::bad_function_call); +} +``` + +三个关键行为:令牌有效时不取消、令牌失效后 void 回调不执行、令牌失效后非 void 回调抛出 `std::bad_function_call`。 + +### F 类:Then 组合 + +```cpp +TEST_CASE("then chains two callbacks", "[then]") { + auto cb = OnceCallback([](int x) { return x * 2; }) + .then([](int x) { return x + 10; }); + int result = std::move(cb).run(5); + REQUIRE(result == 20); // 5 * 2 + 10 +} + +TEST_CASE("then multi-level pipeline", "[then]") { + auto pipeline = OnceCallback([](int x) { return x * 2; }) + .then([](int x) { return x + 10; }) + .then([](int x) { return std::to_string(x); }); + std::string result = std::move(pipeline).run(5); + REQUIRE(result == "20"); +} + +TEST_CASE("then with void first callback", "[then]") { + int value = 0; + auto cb = OnceCallback([&value](int x) { value = x; }) + .then([&value] { return value * 3; }); + int result = std::move(cb).run(7); + REQUIRE(result == 21); +} +``` + +覆盖三种组合模式:两级非 void 管道、多级管道(跨越类型边界从 int 到 string)、void 前缀回调。 + +--- + +## 性能对比:与 Chromium 原版 + +### 对象大小 + +```cpp +std::cout << "sizeof(std::function): " + << sizeof(std::function) << " bytes\n"; +std::cout << "sizeof(std::move_only_function): " + << sizeof(std::move_only_function) << " bytes\n"; +// Chromium OnceCallback ≈ 8 bytes + +std::cout << "sizeof(OnceCallback): " + << sizeof(OnceCallback) << " bytes\n"; +// 我们的:move_only_function (32) + status (1) + token ptr (16) + padding +// 预估 56-64 bytes +``` + +在 GCC 上,典型值是 `std::function` 约 32 字节,`std::move_only_function` 约 32 字节,我们的 `OnceCallback` 约 56-64 字节。Chromium 的只有 8 字节。 + +差距的根源在于存储策略。Chromium 把所有状态放在堆上的 `BindState` 里,回调对象只持有一个指针。我们用 `std::move_only_function` 的 SBO 把小对象直接内联存储,避免了堆分配但增大了对象大小。 + +### 分配行为 + +`std::move_only_function` 的 SBO 阈值通常是 2-3 个指针大小(16-24 字节)。捕获少量参数的 lambda 通常能放进 SBO,不会触发堆分配。大 lambda 则在构造时堆分配。 + +Chromium 总是堆分配(`new BindState`),但分配只发生一次。之后 OnceCallback 的移动操作只是复制一个指针(8 字节),代价极低。我们的方案在小对象时不分配(SBO),但移动操作需要复制 32+ 字节。 + +### 间接调用开销 + +两种方案的调用开销是一样的——一次间接函数调用。`std::move_only_function::operator()` 和 Chromium 的 `polymorphic_invoke_` 都通过函数指针分派。在 `-O2` 优化下,这个间接调用无法被内联消除。 + +### 取舍总结 + +| 指标 | 我们的方案 | Chromium 方案 | +|------|-----------|--------------| +| 回调对象大小 | 56-64 字节 | 8 字节 | +| 小 lambda 堆分配 | 不分配(SBO) | 总是分配 | +| 移动代价 | 复制 32+ 字节 | 复制 1 个指针 | +| 实现代码量 | ~200 行 | ~2000+ 行 | + +我们牺牲了对象的紧凑性和移动操作的极致性能,换来了实现简洁性——不需要手写引用计数、函数指针表、`TRIVIAL_ABI` 注解。小 lambda 的零堆分配在某些低频场景下反而是优势。对教学目的和大多数实际场景来说,这个取舍是值得的。 + +--- + +## 小结 + +这一篇我们做了两件事。测试方面,围绕六个不变量(基本调用、移动语义、单次调用、参数绑定、取消机制、链式组合)设计了 11 个 Catch2 测试用例,覆盖了 OnceCallback 的所有核心行为。性能方面,对比了与 Chromium OnceCallback 在对象大小、分配行为和调用开销上的差异——我们的实现用紧凑性换来了简洁性。 + +到这里,OnceCallback 组件的设计、实现和验证就全部完成了。13 篇文章从前置知识到实战,覆盖了从 C++11 移动语义到 C++23 deducing this 的完整知识链。希望这个系列能帮助你理解"如何用现代 C++ 设计一个工业级的组件"——不仅仅是写代码,更重要的是理解每一个设计决策背后的原因。 + +## 参考资源 + +- [Chromium base/functional/ 源码目录](https://source.chromium.org/chromium/chromium/src/+/main:base/functional/) +- [cppreference: std::move_only_function](https://en.cppreference.com/w/cpp/utility/functional/move_only_function) +- [Catch2 文档](https://github.com/catchorg/Catch2/tree/devel/docs) diff --git a/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/index.md b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/index.md new file mode 100644 index 000000000..674ad8451 --- /dev/null +++ b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/index.md @@ -0,0 +1,57 @@ +# 新手完整教程 + +本目录包含 OnceCallback 组件的完整新手教程,共 13 篇文章,覆盖从 C++ 基础特性复习到组件实现、测试的完整学习路径。 + +## 前置知识 + +先掌握 OnceCallback 所需的 C++ 核心特性: + +1. [OnceCallback 前置知识速查:C++11/14/17 核心特性回顾](pre-00-once-callback-cpp-basics-review.md) +2. [OnceCallback 前置知识(一):函数类型与模板偏特化](pre-01-once-callback-function-type-and-specialization.md) +3. [OnceCallback 前置知识(二):std::invoke 与统一调用协议](pre-02-once-callback-invoke-and-callable.md) +4. [OnceCallback 前置知识(三):Lambda 高级特性](pre-03-once-callback-lambda-advanced.md) +5. [OnceCallback 前置知识(四):Concepts 与 requires 约束](pre-04-once-callback-concepts-and-requires.md) +6. [OnceCallback 前置知识(五):std::move_only_function (C++23)](pre-05-once-callback-move-only-function.md) +7. [OnceCallback 前置知识(六):Deducing this (C++23)](pre-06-once-callback-deducing-this.md) + +## 动手实践 + +学完前置知识后,开始实现 OnceCallback: + +1. [OnceCallback 实战(一):动机与接口设计](01-1-once-callback-motivation-and-api-design.md) +2. [OnceCallback 实战(二):核心骨架搭建](01-2-once-callback-core-skeleton.md) +3. [OnceCallback 实战(三):bind_once 实现](01-3-once-callback-bind-once.md) +4. [OnceCallback 实战(四):取消令牌设计](01-4-once-callback-cancellation-token.md) +5. [OnceCallback 实战(五):then 链式组合](01-5-once-callback-then-chaining.md) +6. [OnceCallback 实战(六):测试与性能对比](01-6-once-callback-testing-and-perf.md) + +## 配套代码 + +前置知识章节中涉及的 C++ 独立示例代码已提炼为可编译的最小工程,位于: + +``` +code/volumn_codes/vol9/full_tutorial_codes/chrome_design/ +``` + +| 示例 | 主题 | 来源文章 | 最低 C++ 标准 | +|------|------|----------|-------------| +| `01_move_semantics.cpp` | 移动语义、完美转发、可变参数模板 | pre-00 | C++17 | +| `02_smart_pointers.cpp` | unique_ptr、shared_ptr | pre-00 | C++17 | +| `03_atomic_memory_order.cpp` | atomic、memory_order、enum class | pre-00 | C++17 | +| `04_lambda_basics.cpp` | 捕获模式、泛型 lambda、[[nodiscard]] | pre-00 | C++17 | +| `05_lambda_advanced.cpp` | mutable lambda、init capture、C++17/C++20 bind | pre-03 | C++20 | +| `06_type_traits.cpp` | type traits、if constexpr、decltype(auto)、ref-qualifier | pre-00 | C++17 | +| `07_function_type_specialization.cpp` | 函数类型、FuncTraits、主模板+偏特化 | pre-01 | C++17 | +| `08_invoke.cpp` | std::invoke、std::invoke_result_t | pre-02 | C++17 | +| `09_concepts_requires.cpp` | concept、requires、not_the_same_t、模板构造函数劫持 | pre-04 | C++20 | +| `10_move_only_function.cpp` | std::move_only_function 构造/移动/判空/SBO | pre-05 | C++23 | +| `11_deducing_this.cpp` | deducing this 推导规则、左值拦截 | pre-06 | C++23 | + +构建方式: + +```bash +cd code/volumn_codes/vol9/full_tutorial_codes/chrome_design +mkdir build && cd build +cmake .. +make -j$(nproc) +``` diff --git a/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-00-once-callback-cpp-basics-review.md b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-00-once-callback-cpp-basics-review.md new file mode 100644 index 000000000..915cedba2 --- /dev/null +++ b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-00-once-callback-cpp-basics-review.md @@ -0,0 +1,366 @@ +--- +title: "OnceCallback 前置知识速查:C++11/14/17 核心特性回顾" +description: "一篇快速复习 OnceCallback 系列所需的所有 C++ 基础特性——移动语义、完美转发、可变参数模板、智能指针、atomic、lambda、类型特征等,为后续深度学习做好准备" +chapter: 0 +order: 0 +tags: + - host + - cpp-modern + - intermediate + - 基础 + - 入门 +difficulty: intermediate +platform: host +cpp_standard: [11, 14, 17, 20] +reading_time_minutes: 25 +prerequisites: + - "卷一 C++ 基础入门" +related: + - "OnceCallback 前置知识(一):函数类型与模板偏特化" + - "OnceCallback 前置知识(三):Lambda 高级特性" +--- + +# OnceCallback 前置知识速查:C++11/14/17 核心特性回顾 + +## 引言 + +说实话,这一篇不是给你"从零讲明白"的——如果你对移动语义、智能指针这些概念完全陌生,建议先回到卷二把对应章节啃完再回来。这一篇的角色是**速查手册**:我们把 OnceCallback 系列后面会反复用到的 C++ 特性全部拉出来过一遍,每个特性只讲三件事——"它是什么"、"怎么用"、"OnceCallback 里哪里会用到"。目的是让你在读后续文章的时候不会因为某个语法细节卡住。 + +> **学习目标** +> +> - 快速回顾 OnceCallback 系列所需的全部 C++11/14/17 基础特性 +> - 理解每个特性在 OnceCallback 设计中的具体应用位置 +> - 建立后续深度学习所需的知识基线 + +--- + +## 移动语义与 std::move + +移动语义是整个 OnceCallback 的根基——它是一个 move-only 类型,核心设计全靠移动语义撑着。我们先快速过一遍核心概念。 + +### 右值引用与移动构造 + +C++11 引入了右值引用 `T&&`,它能绑定到临时对象(右值)上。移动构造函数 `T(T&& other)` 的语义是"从 `other` 那里把资源**偷**过来,而不是复制一份"。偷完之后,`other` 进入一个"有效但未指定"的状态——通常是被清空。 + +```cpp +// 一个最简单的移动语义示例 +class Buffer { + int* data_; + std::size_t size_; +public: + // 普通构造 + Buffer(std::size_t n) : data_(new int[n]), size_(n) {} + // 移动构造:偷走 other 的资源 + Buffer(Buffer&& other) noexcept + : data_(other.data_), size_(other.size_) { + other.data_ = nullptr; // 清空源对象 + other.size_ = 0; + } + ~Buffer() { delete[] data_; } +}; + +Buffer a(100); // a 拥有 100 个 int +Buffer b = std::move(a); // b 偷走了 a 的资源,a 变空 +``` + +### std::move 的本质 + +`std::move` 其实什么都不移动——它只是一个 `static_cast`,把传入的对象无条件转换成右值引用。真正执行"移动"的是移动构造函数或移动赋值运算符。`std::move` 的角色是告诉编译器"我同意把这个对象当作右值对待,你可以从它那里偷资源"。 + +### 在 OnceCallback 中的应用 + +OnceCallback 的调用方式是 `std::move(cb).run(args...)`——`std::move` 把 `cb` 转成右值,`run()` 通过 deducing this(C++23 特性,后面有专门一篇文章讲)检测到这是一个右值调用,执行回调并把 `cb` 的状态标记为"已消费"。之后任何对 `cb` 的访问都是非法的。整个设计思路就是:**通过类型系统来强制约束"调用一次即失效"的语义**。 + +OnceCallback 同时删除了拷贝构造和拷贝赋值(`= delete`),只保留移动操作。这意味着一个 OnceCallback 对象在任意时刻只有一个持有者——你没法复制它,只能通过 `std::move` 转移所有权。 + +--- + +## 完美转发与 std::forward + +完美转发解决的问题是:你写了一个函数模板,它接受参数并原封不动地传给另一个函数。所谓"原封不动"是指保持参数的值类别(左值还是右值)和 const 修饰。 + +### 转发引用与推导规则 + +当函数模板的参数是 `T&&` 且 `T` 是模板参数时,`T&&` 不是普通的右值引用,而是**转发引用**(也叫万能引用)。编译器会根据传入参数的值类别来推导 `T`: + +- 传入左值 `x`(类型 `int`)→ `T = int&`,`T&&` 折叠为 `int&` +- 传入右值 `42`(类型 `int`)→ `T = int`,`T&&` 就是 `int&&` + +### std::forward 的作用 + +`std::forward(arg)` 根据模板参数 `T` 的类型决定是返回左值引用还是右值引用: + +```cpp +template +void wrapper(T&& arg) { + // std::forward 保持 arg 的原始值类别 + target(std::forward(arg)); +} + +int x = 10; +wrapper(x); // arg 是左值引用,forward 返回左值引用 +wrapper(10); // arg 是右值引用,forward 返回右值引用 +``` + +如果你不用 `std::forward` 而直接传 `arg`,那 `arg` 在函数内部永远是左值(因为具名变量都是左值),右值信息就丢失了。 + +### 在 OnceCallback 中的应用 + +完美转发在 OnceCallback 里出现了很多次。`bind_once` 函数模板用它来保持绑定参数的值类别——`std::forward(args)...` 确保传入的右值仍然是右值,传入的左值仍然是左值。`run()` 方法的 deducing this 实现里也用到了 `std::forward(self)` 来将 `self` 的值类别完美转发给内部的 `impl_run`。 + +--- + +## 可变参数模板与参数包展开 + +可变参数模板让你写出一个接受任意数量、任意类型参数的函数或类。OnceCallback 的模板签名 `OnceCallback` 就用到了参数包。 + +### 基本语法 + +```cpp +template // Types 是参数包 +void print_all(Types... args) { + // args... 在这里展开 + // sizeof...(Types) 返回参数数量 +} +``` + +`Types...` 叫做参数包(parameter pack),它可以包含零个或多个类型。`args...` 是函数参数包,在调用时展开。`sizeof...(Types)` 是编译期常量,返回包中元素的数量。 + +### 展开位置 + +参数包可以在多个位置展开:函数参数列表、模板参数列表、初始化列表、捕获列表(C++20 起)等。OnceCallback 里最关键的一个展开位置是 lambda 的捕获列表——这个特性在 C++20 才引入,我们后面有专门一篇文章讲。 + +### 在 OnceCallback 中的应用 + +`OnceCallback` 的 `Args...` 就是一个参数包,它在类的整个实现中反复出现——构造函数的参数类型、`run()` 的参数类型、内部 `func_` 的签名,全部来自这个包。`bind_once` 的 `BoundArgs...` 是另一个参数包,展开到 lambda 的捕获列表和 `std::invoke` 的调用参数中。 + +--- + +## 智能指针速查 + +OnceCallback 内部用到了两种智能指针,我们快速过一下各自的角色。 + +### std::unique_ptr:独占所有权 + +`unique_ptr` 是独占式的智能指针——同一时刻只有一个 `unique_ptr` 指向对象。它不可拷贝,只能移动。创建方式是 `std::make_unique(args...)`。 + +```cpp +auto p = std::make_unique(42); +// auto p2 = p; // 编译错误:不可拷贝 +auto p3 = std::move(p); // OK:移动转移所有权 +// 此后 p 为 nullptr +``` + +在 OnceCallback 中,`unique_ptr` 的意义不在于我们直接使用它,而在于 OnceCallback 必须支持捕获 move-only 对象的 lambda——如果一个 lambda 捕获了 `unique_ptr`,那么包含这个 lambda 的 `std::move_only_function`(OnceCallback 的内部存储)也必须是 move-only 的。这是 `std::function` 做不到的,也是我们选择 `std::move_only_function` 的原因之一。 + +### std::shared_ptr:共享所有权 + +`shared_ptr` 通过引用计数管理对象生命周期。所有指向同一对象的 `shared_ptr` 共享同一个引用计数,最后一个 `shared_ptr` 被销毁时对象也被销毁。 + +```cpp +auto p1 = std::make_shared(42); +auto p2 = p1; // OK:拷贝,引用计数 +1 +// p1 和 p2 都指向同一个 int +``` + +在 OnceCallback 中,`shared_ptr` 用于管理取消令牌 `CancelableToken`。令牌需要在 OnceCallback 对象和外部控制方之间共享——外部控制方调用 `invalidate()` 使令牌失效,OnceCallback 在执行回调前通过自己持有的 `shared_ptr` 副本检查令牌状态。`shared_ptr` 的引用计数保证了只要还有人持有令牌,底层的 `Flag` 对象就不会被销毁。 + +--- + +## std::atomic 与 memory_order + +取消令牌的内部实现用到了 `std::atomic` 和 `memory_order_acquire/release`。 + +### 原子操作 + +`std::atomic` 提供对 `T` 类型变量的原子访问——读和写不会被其他线程的操作打断。基本操作是 `load()`(读)和 `store()`(写),可以指定内存序。 + +```cpp +std::atomic flag{true}; + +// 线程 A:写入 +flag.store(false, std::memory_order_release); + +// 线程 B:读取 +if (flag.load(std::memory_order_acquire)) { + // flag 仍然为 true +} +``` + +### acquire/release 语义 + +`memory_order_release` 和 `memory_order_acquire` 是一对配对的内存序。简单说:`release` store 保证了在 store 之前的所有写操作对其他线程可见;`acquire` load 保证了在 load 之后的所有读操作能看到 release store 之前的写入。在 OnceCallback 的取消令牌中,`invalidate()` 用 `release` store 把 `valid` 设为 `false`,`is_valid()` 用 `acquire` load 读取 `valid`——这保证了如果 `is_valid()` 返回 `true`,令牌相关的所有状态对当前线程都是可见的。 + +--- + +## enum class + +`enum class` 是 C++11 引入的作用域枚举,解决的是老式 `enum` 的名字污染和隐式转换问题。 + +```cpp +// 老式 enum:名字污染全局命名空间,可以隐式转成 int +enum Color { Red, Green, Blue }; +int x = Red; // OK,隐式转换 + +// enum class:名字被限定在枚举作用域内,不可隐式转换 +enum class Status : uint8_t { + kEmpty, // 从未被赋值 + kValid, // 持有有效的可调用对象 + kConsumed // 已被 run() 消费 +}; +Status s = Status::kValid; +// int y = s; // 编译错误:不可隐式转换 +``` + +OnceCallback 用 `enum class Status` 来区分回调的三种状态。底层类型指定为 `uint8_t` 是为了节省内存——整个枚举只占 1 个字节。 + +--- + +## Lambda 基础 + +Lambda 在 OnceCallback 中无处不在——构造回调、`bind_once`、`then()` 的内部实现全部依赖 lambda。这里快速复习基础语法。 + +```cpp +auto add = [](int a, int b) { return a + b; }; +// add 的类型是编译器生成的唯一闭包类 + +int x = 10; +// 值捕获:拷贝 x +auto f1 = [x]() { return x; }; +// 引用捕获:引用 x(注意生命周期) +auto f2 = [&x]() { return x; }; +// 初始化捕获(C++14):可以移动捕获 +auto f3 = [p = std::make_unique(42)]() { return *p; }; +``` + +Lambda 生成的闭包类的 `operator()` 默认是 `const` 的——这意味着你不能在 lambda 内部修改值捕获的变量,除非加上 `mutable` 关键字。在 OnceCallback 的 `bind_once` 和 `then()` 实现中,lambda 必须声明为 `mutable`,因为内部需要调用 `std::move(self).run()` 来修改 `self` 的状态。这个细节我们在 Lambda 高级特性那篇文章里会展开讲。 + +泛型 lambda(C++14 起)允许参数使用 `auto`: + +```cpp +auto generic = [](auto x, auto y) { return x + y; }; +// 编译器为 operator() 生成模板版本 +``` + +`bind_once` 内部的 lambda 用 `(auto&&... call_args)` 来接受运行时参数——这里的 `auto&&` 是转发引用(因为 `auto` 等同于模板参数)。 + +--- + +## 类型特征(Type Traits) + +类型特征是编译期查询和操作类型信息的工具。OnceCallback 里用到了几个关键的 traits,我们快速过一遍。 + +```cpp +#include + +// std::decay_t:去掉 T 上的引用、const/volatile 限定符,数组变指针,函数变函数指针 +using T1 = std::decay_t; // T1 = int +using T2 = std::decay_t; // T2 = OnceCallback(去掉引用) + +// std::is_same_v:A 和 B 是否是同一类型 +static_assert(std::is_same_v); // 通过 +static_assert(!std::is_same_v); // 通过 + +// std::is_lvalue_reference_v:T 是否是左值引用类型 +static_assert(std::is_lvalue_reference_v); // 通过 +static_assert(!std::is_lvalue_reference_v); // 通过 +static_assert(!std::is_lvalue_reference_v); // 通过 + +// std::is_void_v:T 是否是 void +static_assert(std::is_void_v); // 通过 +static_assert(!std::is_void_v); // 通过 +``` + +在 OnceCallback 中,`std::decay_t` 和 `std::is_same_v` 用于 `not_the_same_t` concept——它检查"模板参数退化后是否和 `OnceCallback` 本身是同一类型",用来防止模板构造函数劫持移动构造函数的调用。`std::is_lvalue_reference_v` 用于 `run()` 的 deducing this 实现——检测调用方是否传了左值,如果是就触发 `static_assert` 报错。`std::is_void_v` 用于 `impl_run()` 和 `then()` 中区分 void 和非 void 返回类型的编译期分支。 + +--- + +## if constexpr + +`if constexpr` 是 C++17 引入的编译期条件分支。它和普通 `if` 的区别在于:条件必须是编译期常量表达式,**未选中的分支不会被编译**——甚至连语法检查都不会做。这个特性在处理 void 返回类型时特别有用。 + +```cpp +template +R do_something() { + if constexpr (std::is_void_v) { + // void 返回:执行操作,不 return + perform_action(); + return; // void return + } else { + // 非 void 返回:执行操作,return 结果 + return perform_action(); + } +} +``` + +如果没有 `if constexpr` 而用普通的 `if`,两边的分支都会被编译。此时 void 分支里的 `return result` 会直接报错——void 不是一种可以赋值的类型。`if constexpr` 保证了 void 的情况只生成 `return;` 的代码,非 void 的情况只生成 `return result;` 的代码。 + +在 OnceCallback 中,`if constexpr (std::is_void_v)` 出现在两个地方:`impl_run()` 的回调执行逻辑,和 `then()` 的链式组合逻辑。两处都是同一个问题——void 返回类型不能用常规方式赋值和返回。 + +--- + +## decltype(auto) + +`decltype(auto)` 是 C++14 引入的返回类型推导方式。它和 `auto` 的区别在于对引用的处理:`auto` 会丢掉引用和顶层 const,`decltype(auto)` 会保留。 + +```cpp +int x = 10; +int& ref = x; + +auto f1() { return ref; } // 返回 int(丢掉了引用) +decltype(auto) f2() { return ref; } // 返回 int&(保留了引用) +``` + +在 OnceCallback 中,`bind_once` 和 `then()` 的 lambda 用 `-> decltype(auto)` 作为尾置返回类型。这样做的目的是完美转发可调用对象的返回值——如果被调用的函数返回 `int&&`,`decltype(auto)` 也会返回 `int&&`,不会丢失值类别信息。 + +--- + +## [[nodiscard]] 属性 + +`[[nodiscard]]` 是 C++17 标准化的属性,告诉编译器"这个函数的返回值不应该被忽略"。如果调用方写了 `cb.is_cancelled();` 但没有使用返回值,编译器会发出警告。 + +```cpp +[[nodiscard]] bool is_cancelled() const noexcept; +[[nodiscard]] bool maybe_valid() const noexcept; +[[nodiscard]] bool is_null() const noexcept; +``` + +OnceCallback 的三个查询方法都标注了 `[[nodiscard]]`。原因很简单——调用这些方法就是为了拿返回值做判断,忽略返回值的调用大概率是手滑写错了(比如把 `if (!cb.is_cancelled())` 写成了 `cb.is_cancelled();`)。`explicit operator bool()` 的 `explicit` 也起类似作用——防止隐式转换到 `bool` 引发的意外行为。 + +--- + +## Ref-qualified 成员函数 + +C++11 允许对非静态成员函数进行引用限定(ref-qualifier),用 `&` 或 `&&` 标注在函数参数列表后面。`&` 表示只能通过左值调用,`&&` 表示只能通过右值调用。 + +```cpp +class Widget { +public: + void process() & { + // 只能通过左值调用:Widget w; w.process(); + } + void process() && { + // 只能通过右值调用:Widget().process(); 或 std::move(w).process(); + } +}; +``` + +在 OnceCallback 中,`then()` 方法声明为 `auto then(Next&& next) &&`——末尾的 `&&` 意味着 `then()` 只能通过右值调用(`std::move(cb).then(next)` 或临时对象上的 `.then(next)`)。这是表达消费语义的另一种方式——和 `run()` 用 deducing this 不同,`then()` 不需要区分左值和右值给出不同的错误信息,直接用 ref-qualifier 更简洁。 + +--- + +## 小结 + +这一篇我们把 OnceCallback 系列会用到的所有 C++ 基础特性快速过了一遍。每个特性我们都明确了三点:它是什么、怎么用、在 OnceCallback 的哪里会出现。如果你对某个特性感到陌生,建议回到对应卷的章节系统学习——后续文章不会再重复解释这些基础语法。 + +接下来我们要进入深度环节了。第一站是"函数类型与模板偏特化"——这是理解 `OnceCallback` 这个古怪写法的关键,也是我们搭建整个模板骨架的入口。 + +## 参考资源 + +- [cppreference: 移动语义与右值引用](https://en.cppreference.com/w/cpp/language/reference) +- [cppreference: std::forward](https://en.cppreference.com/w/cpp/utility/forward) +- [cppreference: 可变参数模板](https://en.cppreference.com/w/cpp/language/parameter_pack) +- [cppreference: std::shared_ptr](https://en.cppreference.com/w/cpp/memory/shared_ptr) +- [cppreference: std::atomic](https://en.cppreference.com/w/cpp/atomic/atomic) +- [cppreference: if constexpr](https://en.cppreference.com/w/cpp/language/if) +- [cppreference: Type traits](https://en.cppreference.com/w/cpp/header/type_traits) diff --git a/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-01-once-callback-function-type-and-specialization.md b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-01-once-callback-function-type-and-specialization.md new file mode 100644 index 000000000..ea6debc47 --- /dev/null +++ b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-01-once-callback-function-type-and-specialization.md @@ -0,0 +1,202 @@ +--- +title: "OnceCallback 前置知识(一):函数类型与模板偏特化" +description: "深入理解函数类型 int(int,int) 是什么,以及 OnceCallback 背后的模板偏特化技巧——编译器如何通过模式匹配拆解函数签名" +chapter: 0 +order: 1 +tags: + - host + - cpp-modern + - intermediate + - 模板 + - 泛型 +difficulty: intermediate +platform: host +cpp_standard: [11, 14, 17, 20] +reading_time_minutes: 10 +prerequisites: + - "OnceCallback 前置知识速查:C++11/14/17 核心特性回顾" +related: + - "OnceCallback 前置知识(五):std::move_only_function" + - "OnceCallback 实战(二):核心骨架搭建" +--- + +# OnceCallback 前置知识(一):函数类型与模板偏特化 + +## 引言 + +如果你第一次看到 `OnceCallback` 这个写法,大概率会觉得有点奇怪——`int(int, int)` 看起来像函数声明,但它出现在模板参数的位置上。这个东西到底是什么?编译器是怎么把 `int(int, int)` 拆解成"返回 int、接受两个 int 参数"这些信息的? + +这一篇我们就来拆解这个看似古怪但实则非常优雅的技巧。理解了它,你就能看懂 `std::function`、`std::move_only_function` 和我们的 `OnceCallback` 的模板签名为什么长成这个样子。 + +> **学习目标** +> +> - 理解函数类型(function type)是 C++ 中的一种合法类型 +> - 掌握"主模板 + 偏特化"这个反复出现的模板设计模式 +> - 能够自己实现一个最小版本的函数签名拆解工具 + +--- + +## 函数类型:C++ 中一个容易被忽略的类型 + +先问一个基本问题:`int(int, int)` 在 C++ 里是一种类型吗? + +答案是:是的。`int(int, int)` 是一种叫做**函数类型**(function type)的东西,它描述的是"接受两个 int 参数、返回 int 的函数"。注意,它不是函数指针 `int(*)(int, int)`,也不是函数引用 `int(&)(int, int)`——函数类型是比函数指针更底层的概念。 + +我们可以用 `static_assert` 来验证: + +```cpp +#include + +static_assert(std::is_function_v); // 通过:是函数类型 +static_assert(!std::is_pointer_v); // 通过:不是指针 +static_assert(std::is_pointer_v); // 通过:这是函数指针 +``` + +函数类型在实际代码中出现的场景比你想象的要多。当你写一个函数声明的时候: + +```cpp +int add(int a, int b); +``` + +`add` 的类型就是 `int(int, int)`。你可以把它想象成一种"签名"——它完整描述了这个函数接受什么参数、返回什么类型,但不涉及函数本身存储在哪里。 + +函数类型和函数指针之间有一个隐式转换:函数名在大多数表达式中会自动退化(decay)成指向自身的指针。这就像数组名退化成指针一样——`int arr[5]` 中的 `arr` 在大多数上下文中会变成 `int*`,`int add(int, int)` 中的 `add` 会变成 `int(*)(int, int)`。 + +但作为**模板参数**传入时,函数类型不会退化——编译器原封不动地接收这个类型。这正是我们能够用模板偏特化来拆解它的前提。 + +--- + +## 主模板 + 偏特化:拆解函数类型的模式 + +现在我们来看看 `OnceCallback` 的模板声明是怎么写的。它用了一个两步走的设计:先声明一个只接受一个类型参数的主模板,再为"这个类型参数恰好是函数类型"的情形提供一个偏特化版本。 + +### 第一步:主模板声明 + +```cpp +template +class OnceCallback; // 主模板:只有声明,没有定义 +``` + +主模板故意不提供实现。这不是遗忘,而是设计——如果有人不小心写出了 `OnceCallback` 这种用法(传了一个普通的 int 类型而不是函数签名),编译器会在实例化时报错,因为找不到定义。这是一种编译期的安全网。 + +### 第二步:偏特化版本 + +```cpp +template +class OnceCallback { + // 所有真正的代码都在这里 +}; +``` + +这个偏特化版本的模板参数列表是 ``,而类名后面跟的 `OnceCallback` 是偏特化的**模式匹配条件**——它说的是:"当 `FuncSignature` 能被拆解成 `ReturnType(FuncArgs...)` 这种形式时,用这个版本。" + +### 编译器的匹配过程 + +当你写 `OnceCallback` 时,编译器做了这么几件事: + +首先,它看到你在实例化 `OnceCallback`,模板参数是 `int(int, int)`。然后它去看主模板 `template class OnceCallback`,把 `FuncSignature` 绑定为 `int(int, int)` 这个整体类型。接下来它去检查有没有偏特化版本能用——偏特化要求 `FuncSignature` 能匹配 `ReturnType(FuncArgs...)` 的模式。`int(int, int)` 恰好可以拆成 `ReturnType = int`、`FuncArgs = {int, int}`,匹配成功!于是偏特化版本被选中。 + +你可以把这个过程想象成一种类型层面的模式匹配——就像正则表达式 `(\w+)\((\w+(?:,\s*\w+)*)\)` 可以从字符串 `int(int, int)` 中提取出返回值和参数列表一样,模板偏特化从类型 `int(int, int)` 中提取出返回类型和参数包。 + +### 和 `std::function` 用的是完全相同的技术 + +如果你去翻 `std::function` 的标准库实现,你会发现它用了完全一样的模式: + +```cpp +// std::function 的简化实现 +template class function; // 主模板 + +template +class function { // 偏特化 + // ... +}; +``` + +`std::move_only_function`(C++23)也是一样的。这个"主模板 + 函数类型偏特化"的模式在标准库里出现了三次,是一个经过充分验证的设计。 + +--- + +## 动手实践:实现一个 FuncTraits + +光看不练容易忘。我们现在自己动手实现一个最小的函数签名拆解工具,用来巩固理解。目标是:给定一个函数类型 `R(Args...)`,能提取出返回类型 `R` 和参数包 `Args...`。 + +```cpp +#include + +// 主模板:对非函数类型不提供定义 +template +struct FuncTraits; + +// 偏特化:拆解函数类型 R(Args...) +template +struct FuncTraits { + using ReturnType = R; + using ArgsTuple = std::tuple; + + static constexpr std::size_t kArity = sizeof...(Args); +}; + +// 验证 +static_assert(std::is_same_v::ReturnType, int>); +static_assert(std::is_same_v::ReturnType, void>); +static_assert(FuncTraits::kArity == 3); +``` + +`FuncTraits` 和 `OnceCallback` 使用了完全相同的偏特化模式。唯一的区别是 `FuncTraits` 把拆出来的类型存成了 `using` 别名和 `static constexpr` 常量,而 `OnceCallback` 直接在偏特化类的内部使用这些类型来定义数据成员和方法。 + +试着编译运行这个示例——如果 `static_assert` 全部通过(没有编译错误),说明偏特化正确地把函数类型拆开了。你可以试着加一些更复杂的类型来测试: + +```cpp +// 更复杂的验证 +static_assert(std::is_same_v< + FuncTraits::ReturnType, + std::string>); +static_assert(std::is_same_v< + FuncTraits::ArgsTuple, + std::tuple>); +``` + +--- + +## 为什么不用 OnceCallback? + +你可能会想,既然目的是拿到返回类型和参数列表,为什么不直接写成 `OnceCallback` 这种形式?像这样: + +```cpp +template +class OnceCallback { + // ... +}; + +// 使用:OnceCallback cb([](int a, int b) { return a + b; }); +``` + +这种写法技术上可行,但用户体验差了一截。对比两种调用方式: + +```cpp +// 签名式:一个模板参数,看起来像函数签名 +OnceCallback cb1([](int a, int b) { return a + b; }); + +// 参数罗列式:返回类型和参数分开写 +OnceCallback cb2([](int a, int b) { return a + b; }); +``` + +第一种更自然——`int(int, int)` 就是一个完整的函数签名,读起来一目了然。第二种需要你在大脑里把第一个 `int` 解读为返回类型、后面的 `int, int` 解读为参数列表,这增加了认知负担。标准库的选择也是签名式——`std::function` 而不是 `std::function`。 + +签名式写法还有一个微妙的好处:它和 C++ 的类型系统更一致。`int(int, int)` 是一个真实的类型,而"一个返回类型加上一组参数类型"不是一个类型——它只是几个类型的罗列。用函数类型作为模板参数,是在类型系统的层面上操作,而不是在语法糖的层面上操作。 + +当然,签名式写法也有一个缺点——编译器没法从可调用对象自动推导出完整的签名。这就是为什么 `bind_once` 的第一个模板参数 `Signature` 必须手动指定的原因,这个取舍我们在后续的 `bind_once` 实现篇里会详细讨论。 + +--- + +## 小结 + +这一篇我们搞清楚了三件事。函数类型 `int(int, int)` 是 C++ 中的一种合法类型,它完整描述了函数的签名,不是函数指针也不是函数引用。"主模板 + 偏特化"这个模式通过模式匹配把函数类型拆解成返回类型和参数包,`std::function`、`std::move_only_function` 和我们的 `OnceCallback` 都用了同样的技巧。签名式写法 `OnceCallback` 比参数罗列式 `OnceCallback` 更自然、更符合 C++ 类型系统的设计哲学。 + +下一篇我们去看 `std::invoke`——它是让 `bind_once` 能够统一处理函数指针、成员函数指针和 lambda 的关键工具。 + +## 参考资源 + +- [cppreference: 函数类型](https://en.cppreference.com/w/cpp/language/function) +- [cppreference: 模板偏特化](https://en.cppreference.com/w/cpp/language/template_specialization) +- [cppreference: std::is_function](https://en.cppreference.com/w/cpp/types/is_function) diff --git a/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-02-once-callback-invoke-and-callable.md b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-02-once-callback-invoke-and-callable.md new file mode 100644 index 000000000..ee0365d4c --- /dev/null +++ b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-02-once-callback-invoke-and-callable.md @@ -0,0 +1,250 @@ +--- +title: "OnceCallback 前置知识(二):std::invoke 与统一调用协议" +description: "深入理解 std::invoke 如何统一函数指针、成员函数指针、lambda、仿函数的调用方式,以及 std::invoke_result_t 在 OnceCallback 中的类型推导作用" +chapter: 0 +order: 2 +tags: + - host + - cpp-modern + - intermediate + - 函数对象 + - std_invoke +difficulty: intermediate +platform: host +cpp_standard: [17] +reading_time_minutes: 10 +prerequisites: + - "OnceCallback 前置知识速查:C++11/14/17 核心特性回顾" + - "OnceCallback 前置知识(一):函数类型与模板偏特化" +related: + - "OnceCallback 实战(三):bind_once 实现" + - "OnceCallback 实战(五):then 链式组合" +--- + +# OnceCallback 前置知识(二):std::invoke 与统一调用协议 + +## 引言 + +假设你在写一个回调系统——就像我们正在做的 OnceCallback。你的系统需要接受各种各样的"可调用对象":普通函数指针、lambda、仿函数(重载了 `operator()` 的类对象),甚至成员函数指针。问题来了,这些可调用对象的调用语法各不相同。普通函数直接 `f(args...)`,成员函数指针必须写成 `(obj.*pmf)(args...)`。如果你的代码里有十种不同的可调用对象,是不是要写十个 `if-else` 分支来分别处理? + +`std::invoke`(C++17)就是为了消灭这种分裂而生的。它提供了一种统一的调用语法,让所有可调用对象都能用同一种方式调用。OnceCallback 的 `bind_once` 和 `then()` 内部全部依赖它来实现"不管传进来什么可调用对象,都能正确调用"这个需求。 + +> **学习目标** +> +> - 理解为什么需要统一调用协议——各种可调用对象的调用语法差异 +> - 掌握 `std::invoke` 的完整分派规则 +> - 学会使用 `std::invoke_result_t` 在编译期推导调用结果的类型 + +--- + +## 问题:可调用对象的调用语法分裂 + +C++ 中至少有四种常见的可调用对象,它们的调用语法各不相同。我们逐一看看。 + +### 普通函数指针 + +```cpp +int add(int a, int b) { return a + b; } +int (*fp)(int, int) = &add; + +int result = fp(3, 4); // 直接调用 +int result2 = (*fp)(3, 4); // 解引用后调用(等价) +``` + +### Lambda / 仿函数 + +```cpp +auto lam = [](int a, int b) { return a + b; }; +int result = lam(3, 4); // 通过 operator() 调用 + +struct Adder { + int operator()(int a, int b) { return a + b; } +}; +Adder fn; +int result2 = fn(3, 4); // 同样通过 operator() 调用 +``` + +### 成员函数指针 + +这里语法开始变得古怪了。成员函数指针不能像普通函数那样直接调用——你必须有一个对象实例,然后用 `.*` 或 `->*` 运算符来调用。 + +```cpp +struct Calculator { + int multiply(int a, int b) { return a * b; } +}; + +Calculator calc; +int (Calculator::*pmf)(int, int) = &Calculator::multiply; + +// 必须用 .* 运算符 +int result = (calc.*pmf)(3, 4); // result == 12 +``` + +### 指向数据成员的指针 + +是的,C++ 允许你获取数据成员的"指针"——它其实是一个偏移量。访问方式也是通过 `.*` 运算符。 + +```cpp +struct Point { + double x, y; +}; + +Point p{1.0, 2.0}; +double Point::*pmx = &Point::x; + +double val = p.*pmx; // val == 1.0 +``` + +问题很清楚了:如果你在写一个模板函数,需要调用一个"不知道具体是什么类型的可调用对象",你没法写出一个统一的调用语法——因为你不知道它是普通函数还是成员函数指针。`std::invoke` 就是来解决这个问题的。 + +--- + +## std::invoke 的分派规则 + +`std::invoke(f, args...)` 的工作是:根据 `f` 和 `args` 的具体类型,选择正确的调用语法。标准规定了以下几种情况(C++ 标准术语叫 INVOKE 表达式): + +### 情况一:成员函数指针 + 对象 + +当 `f` 是指向成员函数的指针,`args` 的第一个元素是对象(或对象的引用、或指向对象的指针)时,`std::invoke` 展开为通过对象调用成员函数。 + +```cpp +struct Calculator { + int multiply(int a, int b) { return a * b; } +}; + +Calculator calc; + +// 通过引用 +std::invoke(&Calculator::multiply, calc, 3, 4); // (calc.*multiply)(3, 4) +// 通过指针 +std::invoke(&Calculator::multiply, &calc, 3, 4); // ((*ptr).*multiply)(3, 4) +``` + +注意第二种情况——当第一个参数是指针(`&calc`)时,`std::invoke` 会自动解引用指针。这个行为在 `bind_once` 绑定成员函数时非常重要。 + +### 情况二:指向数据成员的指针 + 对象 + +当 `f` 是指向数据成员的指针时,`std::invoke` 展开为通过对象访问数据成员。 + +```cpp +struct Point { double x, y; }; +Point p{1.0, 2.0}; + +double val = std::invoke(&Point::x, p); // p.*&Point::x == p.x +``` + +### 情况三:其他可调用对象 + +当 `f` 是函数指针、lambda、仿函数等"可以直接调用的东西"时,`std::invoke` 就是简单的 `f(args...)`。 + +```cpp +std::invoke([](int a, int b) { return a + b; }, 3, 4); // lambda(3, 4) +``` + +### 统一接口 + +关键在于,不管 `f` 是上面哪种情况,调用语法都是 `std::invoke(f, args...)`。在你的模板代码里,你不需要知道 `f` 的具体类型——`std::invoke` 在内部帮你分派到正确的调用语法。 + +--- + +## std::invoke_result_t:编译期推导返回类型 + +光有统一调用还不够——有时候你还需要在编译期知道 `std::invoke(f, args...)` 的返回类型是什么。比如在 `then()` 的实现中,我们需要推导"把前一个回调的返回值传给下一个回调,返回什么类型"。 + +`std::invoke_result_t` 就是干这个的。给定可调用对象类型 `F` 和参数类型 `Args...`,它在编译期计算出 `std::invoke(f, args...)` 的返回类型。 + +```cpp +#include +#include + +auto add(int a, int b) -> int { return a + b; } + +// 编译期推导 add(1, 2) 的返回类型 +using R = std::invoke_result_t; +static_assert(std::is_same_v); + +// 对 lambda 也能推导 +auto lam = [](double x) { return std::to_string(x); }; +using R2 = std::invoke_result_t; +static_assert(std::is_same_v); +``` + +### 在 OnceCallback 中的使用 + +`then()` 的实现用 `std::invoke_result_t` 来推导链式调用中新回调的返回类型。具体来说,当 `then()` 接受一个后续回调 `next` 时,它需要知道 `next(上一个回调的返回值)` 会返回什么类型: + +```cpp +// 在 then() 的非 void 分支中 +using NextRet = std::invoke_result_t; +// NextRet 就是"把 ReturnType 类型的值传给 next,返回什么类型" +``` + +void 分支中,后续回调不接受参数: + +```cpp +// 在 then() 的 void 分支中 +using NextRet = std::invoke_result_t; +// next 不接受参数,直接调用 +``` + +--- + +## 在 OnceCallback 源码中的具体使用 + +让我们对照实际源码,看看 `std::invoke` 在 OnceCallback 中的两个使用场景。 + +### bind_once 中的 std::invoke + +```cpp +// bind_once 的 lambda 内部 +return std::invoke( + std::move(f), + std::move(bound)..., + std::forward(call_args)... +); +``` + +这里 `f` 可能是任何可调用对象——普通 lambda、成员函数指针,甚至指向数据成员的指针。如果不用 `std::invoke` 而是直接写 `f(bound..., call_args...)`,当 `f` 是成员函数指针时就会编译失败——因为成员函数指针不能直接用 `()` 调用。 + +### then() 中的 std::invoke + +```cpp +// then() 的非 void 分支 +auto mid = std::move(self).run(std::forward(args)...); +return std::invoke(std::move(cont), std::move(mid)); +``` + +`cont`(后续回调)在 `then()` 的设计里是一个普通的可调用对象(通常是 lambda),不是 `OnceCallback`。所以理论上直接 `cont(mid)` 也能工作——大部分情况下确实如此。但使用 `std::invoke` 是一种防御性编程:如果有人传进来一个成员函数指针作为后续回调,直接调用语法会失败,`std::invoke` 不会。统一使用 `std::invoke` 保证了无论传什么可调用对象都能正确工作,不需要额外的代码来处理特殊类型。 + +--- + +## 踩坑预警:成员函数绑定的生命周期陷阱 + +`std::invoke` 能统一处理成员函数指针,但它不会帮你管理对象的生命周期。当你在 `bind_once` 中绑定一个成员函数时: + +```cpp +struct Calculator { + int multiply(int a, int b) { return a * b; } +}; + +Calculator calc; +auto bound = bind_once(&Calculator::multiply, &calc, 5); +``` + +`&calc` 是一个裸指针,`bind_once` 会把它存到 lambda 的捕获列表里。如果 `calc` 在回调被调用之前就被销毁了,lambda 内部持有的就是一个悬空指针,`std::invoke` 通过悬空指针访问已释放的内存——未定义行为,大概率段错误。 + +Chromium 用 `base::Unretained` 显式标记"我知道这个裸指针的生命周期是安全的",用 `base::Owned` 接管对象的所有权,用 `base::WeakPtr` 在对象析构时自动取消回调。我们的简化版暂时不提供这些保护机制——安全责任在调用方手上。这是一个重要的设计取舍,我们在实战篇里会再提到。 + +--- + +## 小结 + +这一篇我们弄清楚了 `std::invoke` 的来龙去脉。核心动机是各种可调用对象的调用语法各不相同——普通函数直接 `f(args...)`,成员函数指针要 `(obj.*pmf)(args...)`,数据成员指针要 `obj.*pmd`。`std::invoke` 把这些全部统一成 `std::invoke(f, args...)` 一种语法,配合 `std::invoke_result_t` 可以在编译期推导调用的返回类型。在 OnceCallback 中,`bind_once` 和 `then()` 都依赖它来实现"不关心可调用对象的具体类型,只要能调用就行"的泛型设计。 + +下一篇我们去看 Lambda 的高级特性——特别是 C++20 引入的 lambda init capture 包展开,它是 `bind_once` 得以简洁实现的关键。 + +## 参考资源 + +- [cppreference: std::invoke](https://en.cppreference.com/w/cpp/utility/functional/invoke) +- [cppreference: std::invoke_result](https://en.cppreference.com/w/cpp/types/result_of) +- [cppreference: Callable](https://en.cppreference.com/w/cpp/named_req/Callable) diff --git a/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-03-once-callback-lambda-advanced.md b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-03-once-callback-lambda-advanced.md new file mode 100644 index 000000000..20a3209da --- /dev/null +++ b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-03-once-callback-lambda-advanced.md @@ -0,0 +1,233 @@ +--- +title: "OnceCallback 前置知识(三):Lambda 高级特性" +description: "深入讲解 mutable lambda、初始化捕获(init capture)、C++20 lambda capture pack expansion 和泛型 lambda——OnceCallback 中 bind_once 与 then() 的核心实现技巧" +chapter: 0 +order: 3 +tags: + - host + - cpp-modern + - intermediate + - lambda + - 函数对象 +difficulty: intermediate +platform: host +cpp_standard: [14, 17, 20, 23] +reading_time_minutes: 11 +prerequisites: + - "OnceCallback 前置知识速查:C++11/14/17 核心特性回顾" +related: + - "OnceCallback 实战(三):bind_once 实现" + - "OnceCallback 实战(五):then 链式组合" +--- + +# OnceCallback 前置知识(三):Lambda 高级特性 + +## 引言 + +上一篇速查里我们快速过了一遍 lambda 的基础语法。这篇我们要深入到 OnceCallback 实现中真正用到的三个 lambda 高级特性——它们不是什么"锦上添花"的语法糖,而是 `bind_once` 和 `then()` 得以实现的**关键机制**。如果不理解这些特性,后面的实现代码你会看得很痛苦。 + +具体来说,我们要讲三件事:`mutable` lambda 为什么在 OnceCallback 里不能省、初始化捕获(init capture)怎么让 `then()` 把整个 OnceCallback 对象搬进 lambda 里、以及 C++20 的 lambda capture pack expansion 怎么让 `bind_once` 的代码量缩减到原来的三分之一。 + +> **学习目标** +> +> - 理解 `mutable` lambda 与 const lambda 的行为差异及其在 OnceCallback 中的必要性 +> - 掌握初始化捕获的语法和语义,理解 `self = std::move(*this)` 的所有权转移 +> - 学会 C++20 lambda capture pack expansion,理解 `bind_once` 的简洁实现 +> - 理解泛型 lambda `(auto&&... args)` 的本质 + +--- + +## mutable lambda:为什么在 OnceCallback 里不能省 + +Lambda 默认生成的 `operator()` 是 `const` 的——这意味着 lambda 内部不能修改值捕获的变量。加 `mutable` 关键字后,`operator()` 变成非 const 的,允许修改。 + +### 行为对比 + +```cpp +int x = 10; + +// const lambda:不能修改捕获的变量 +auto f1 = [x]() { + // x++; // 编译错误:operator() 是 const 的 + return x; +}; + +// mutable lambda:可以修改捕获的变量 +auto f2 = [x]() mutable { + x++; // OK:operator() 是非 const 的 + return x; +}; + +f2(); // 返回 11,x 的副本被修改 +f2(); // 返回 12,同一个 lambda 对象再次调用,x 继续增加 +``` + +注意第二个例子——`mutable` lambda 的状态在多次调用之间是保持的。这是因为 lambda 的闭包对象持有捕获变量的副本,`mutable` 让 `operator()` 可以修改这些副本。 + +### 在 OnceCallback 中的角色 + +`bind_once` 和 `then()` 的 lambda 都必须声明为 `mutable`。原因是这些 lambda 的捕获列表里包含 `OnceCallback` 对象(通过 `self = std::move(*this)` 捕获),而调用 `std::move(self).run()` 会修改 `self` 的内部状态(把 `status_` 从 kValid 改为 kConsumed)。如果 lambda 是 const 的,`self` 在 lambda 内部就是 const 引用,你没法在 const 对象上调用修改状态的操作——编译器会直接报错。 + +简单说:**一旦 lambda 捕获了需要在调用时被修改的对象(比如 OnceCallback),就必须加 `mutable`**。这不是可选的——不加就编译不过。 + +```cpp +// then() 内部的 lambda——mutable 不可省略 +[self = std::move(*this), cont = std::forward(next)] +(FuncArgs... args) mutable -> NextRet { + // self 在这里需要被修改(run() 会消费它) + auto mid = std::move(self).run(std::forward(args)...); + return std::invoke(std::move(cont), std::move(mid)); +} +``` + +--- + +## 初始化捕获(Init Capture):把对象搬进 lambda + +C++14 引入了初始化捕获(init capture)语法,允许你在捕获列表中执行表达式并用结果初始化一个捕获变量。语法是 `name = expression`。 + +### 和简单捕获的区别 + +简单捕获 `[x]` 只能捕获已经存在的变量,而且是拷贝或引用语义。初始化捕获 `[name = expr]` 允许你做三件简单捕获做不到的事: + +```cpp +auto ptr = std::make_unique(42); + +// 1. 移动捕获——把 unique_ptr 搬进 lambda +auto f1 = [p = std::move(ptr)]() { return *p; }; +// ptr 在外面已经被搬空了 + +// 2. 存储计算结果 +std::string s = "hello"; +auto f2 = [len = s.size()]() { return len; }; // len 是 size_t 类型 + +// 3. 捕获不存在于外部的变量 +auto f3 = [counter = 0]() mutable { return ++counter; }; // counter 是 lambda 自己的变量 +``` + +### 在 OnceCallback 中的使用 + +`then()` 的实现用初始化捕获做了两件关键的事情。 + +第一件是把整个 OnceCallback 对象搬进 lambda: + +```cpp +self = std::move(*this) +``` + +`*this` 是当前 OnceCallback 对象,`std::move(*this)` 把它转成右值,初始化捕获 `self = std::move(*this)` 触发 OnceCallback 的移动构造,把 `func_`、`status_`、`token_` 全部搬进 lambda 的闭包对象里。移动之后,`*this`(原来的 OnceCallback 对象)进入"被移走"的状态——`func_` 和 `token_` 已经是空的或 null 了。 + +第二件是把后续回调搬进来: + +```cpp +cont = std::forward(next) +``` + +`std::forward(next)` 保持 `next` 的值类别——如果传入的是右值,它就是移动;如果传入的是左值,它就是拷贝。通常 `then()` 接受的都是临时 lambda(右值),所以这里是移动。 + +### 所有权链 + +把这两件捕获放在一起看,`then()` 创建的新 lambda 持有了原回调和后续回调的**完整所有权**。这个 lambda 又被存入一个新的 `OnceCallback` 的 `std::move_only_function` 里。整个所有权链条是这样的: + +```text +新 OnceCallback -> move_only_function -> lambda 闭包 -> [原 OnceCallback + 后续回调] +``` + +每一层都通过移动语义传递所有权,没有任何共享或拷贝。这就是 OnceCallback 的 move-only 语义在 `then()` 中的完整体现——所有权从外到内层层传递,没有破绽。 + +--- + +## C++20 Lambda Capture Pack Expansion:bind_once 的简洁秘诀 + +这是这一篇里最重要的特性,也是 `bind_once` 得以用几行代码实现的关键。C++20 之前,可变参数模板的参数包**不能**直接展开到 lambda 的捕获列表里——你得先用 `std::tuple` 把参数打包存起来,然后在 lambda 内部用 `std::apply` 展开调用。 + +### 旧方案(C++17):tuple + apply + +```cpp +template +auto bind_old(F&& f, BoundArgs&&... args) { + // 把所有绑定参数打包进 tuple + return [f = std::forward(f), + tup = std::make_tuple(std::forward(args)...)] + (auto&&... call_args) mutable -> decltype(auto) { + // 用 std::apply 展开 tuple 并调用 + return std::apply([&](auto&... bound) -> decltype(auto) { + return f(bound..., std::forward(call_args)...); + }, tup); + }; +} +``` + +能工作,但代码膨胀了不少——你需要一个中间的 tuple、一个 `std::apply` 调用、以及一个嵌套 lambda 来处理展开。 + +### 新语法(C++20):直接在捕获列表里展开包 + +C++20 允许在 lambda 的初始化捕获中使用包展开。语法是 `...name = expression`,效果是为参数包中的每一个类型生成一个对应的捕获变量。 + +```cpp +template +auto bind_new(F&& f, BoundArgs&&... args) { + return [f = std::forward(f), + ...bound = std::forward(args)] // ← 包展开! + (auto&&... call_args) mutable -> decltype(auto) { + return std::invoke(std::move(f), + std::move(bound)..., // ← 展开捕获变量 + std::forward(call_args)...); + }; +} +``` + +### 手动展开一个具体例子 + +假设我们调用 `bind_new([](int a, std::string b, int c) { ... }, 10, std::string("hello"))`,此时 `BoundArgs = {int, std::string}`。编译器把包展开 `...bound = std::forward(args)` 展开成: + +```cpp +[f = std::forward(f), + b1 = std::forward(arg1), // int 直接转发 + b2 = std::forward(arg2)] // std::string 移动转发 +(auto&&... call_args) mutable -> decltype(auto) { + return std::invoke(std::move(f), + std::move(b1), std::move(b2), // 展开捕获变量 + std::forward(call_args)...); +} +``` + +每个绑定参数变成了 lambda 闭包中的一个独立成员变量,在 lambda 被调用时通过 `std::move(bound)...` 一起展开传给 `std::invoke`。 + +### 为什么用 std::move 而不是 std::forward + +你可能注意到 lambda 内部用的是 `std::move(bound)...` 而不是 `std::forward(bound)...`。原因是 lambda 是 `mutable` 的,捕获变量 `bound` 在 lambda 内部是**左值**(具名变量永远是左值)。由于我们希望绑定参数在回调被调用时以右值的方式传出(触发移动语义),所以用 `std::move` 把它们转成右值。如果用 `std::forward`,因为 `bound` 已经是左值了,`std::forward` 只会返回左值引用——移动语义就丢失了。 + +--- + +## 泛型 Lambda:auto&& 作为转发引用 + +`bind_once` 内部的 lambda 用 `(auto&&... call_args)` 来接受运行时传入的参数。这里的 `auto&&` 是转发引用——因为 `auto` 在 lambda 参数中等同于模板参数,所以 `auto&&` 具有和 `T&&`(T 是模板参数时)相同的推导规则。 + +```cpp +auto f = [](auto&& x) { + // x 是转发引用 + // 传入左值:auto = int&, x 的类型是 int&(左值引用) + // 传入右值:auto = int, x 的类型是 int&&(右值引用) +}; + +int v = 10; +f(v); // x 绑定到左值 +f(10); // x 绑定到右值 +``` + +`auto&&...` 的组合意味着这个 lambda 可以接受任意数量、任意类型的参数,同时保持每个参数的值类别信息。配合 `std::forward(call_args)...`,这些参数可以被完美转发到最终的可调用对象。 + +--- + +## 小结 + +这一篇我们掌握了 OnceCallback 实现中最关键的三个 lambda 特性。`mutable` lambda 允许在 lambda 内部修改捕获的对象,OnceCallback 的 `bind_once` 和 `then()` 必须用它才能在 lambda 里调用 `std::move(self).run()` 修改回调状态。初始化捕获 `name = expr` 让 `then()` 能把整个 OnceCallback 对象通过移动语义搬进 lambda 闭包,建立起完整的所有权链。C++20 的 lambda capture pack expansion `...name = expr` 让 `bind_once` 的绑定参数可以直接展开到捕获列表中,替代了 C++17 时代臃肿的 tuple + apply 方案。 + +下一篇我们去看 Concepts 和 `requires` 约束——它们是保护 OnceCallback 的模板构造函数不被错误匹配的关键防御手段。 + +## 参考资源 + +- [cppreference: Lambda 表达式](https://en.cppreference.com/w/cpp/language/lambda) +- [P0780R2 - Pack Expansion in Lambda Init-Capture](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0780r2.html) +- [cppreference: std::forward](https://en.cppreference.com/w/cpp/utility/forward) diff --git a/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-04-once-callback-concepts-and-requires.md b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-04-once-callback-concepts-and-requires.md new file mode 100644 index 000000000..5af868c9a --- /dev/null +++ b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-04-once-callback-concepts-and-requires.md @@ -0,0 +1,240 @@ +--- +title: "OnceCallback 前置知识(四):Concepts 与 requires 约束" +description: "从模板构造函数劫持移动构造函数的真实问题出发,理解 Concepts 和 requires 约束如何保护 OnceCallback 的构造函数正确匹配" +chapter: 0 +order: 4 +tags: + - host + - cpp-modern + - intermediate + - concepts + - 模板 +difficulty: intermediate +platform: host +cpp_standard: [20] +reading_time_minutes: 11 +prerequisites: + - "OnceCallback 前置知识速查:C++11/14/17 核心特性回顾" + - "OnceCallback 前置知识(一):函数类型与模板偏特化" +related: + - "OnceCallback 实战(二):核心骨架搭建" + - "OnceCallback 前置知识(五):std::move_only_function" +--- + +# OnceCallback 前置知识(四):Concepts 与 requires 约束 + +## 引言 + +OnceCallback 的构造函数上有这么一行看起来很多余的约束: + +```cpp +template + requires not_the_same_t +explicit OnceCallback(Functor&& function); +``` + +你可能会问——为什么不直接写 `template` 就完事了?多加一个 `requires not_the_same_t` 是在防什么? + +这一篇我们就来回答这个问题。答案涉及 C++ 重载决议中一个不太为人知的陷阱:**模板构造函数可能在某些情况下劫持移动构造函数的调用**。Concepts 和 `requires` 约束是 C++20 给我们的防御武器。 + +> **学习目标** +> +> - 理解模板构造函数与移动构造函数之间的重载竞争问题 +> - 掌握 concept 的基本语法和 `requires` 子句的用法 +> - 能够解读 `not_the_same_t` 的设计意图和每一行代码的含义 + +--- + +## 问题引入:模板构造函数的"越位" + +### 场景还原 + +假设我们有一个简单的包装类,接受任意可调用对象: + +```cpp +template +class Callback; + +template +class Callback { +public: + // 模板构造函数:接受任意可调用对象 + template + explicit Callback(Functor&& f) { + // 用 f 初始化内部存储... + } + + // 编译器隐式生成的移动构造函数 + // Callback(Callback&& other) noexcept; +}; +``` + +现在我们写 `Callback cb2 = std::move(cb1);`——意图很明显,我们想调用移动构造函数。编译器面前有两条路: + +1. 隐式生成的移动构造函数 `Callback(Callback&&)` +2. 模板构造函数实例化 `Callback(Callback&&)`(令 `Functor = Callback`) + +直觉上我们会觉得移动构造函数应该优先——毕竟它是"专门为这种类型设计的"。但 C++ 的重载决议规则不是这么简单。在某些情况下,模板实例化出来的函数签名比隐式声明的特殊成员函数是"更精确"的匹配——因为模板参数 `Functor` 可以完美匹配传入参数的类型(包括 `Callback&&`),而移动构造函数的参数类型是固定的 `Callback&&`。 + +当两个重载的匹配程度相同时,C++ 规则规定**非模板函数优先于模板函数**。所以大多数情况下移动构造函数确实会赢。但边缘情况比较微妙——特别是当涉及到转发引用和完美匹配时,有些编译器版本可能会有不同行为。更关键的是,即使移动构造函数赢了,如果模板构造函数也在候选列表中,某些 SFINAE 场景可能导致意外的编译错误。 + +### 最小复现 + +```cpp +struct Wrapper { + // 模板构造函数:接受任何类型 + template + Wrapper(T&& x) { + std::cout << "template constructor\n"; + } + + // 移动构造函数(编译器隐式生成或显式声明) + Wrapper(Wrapper&& other) noexcept { + std::cout << "move constructor\n"; + } +}; + +Wrapper a; +Wrapper b = std::move(a); // 你期望输出 "move constructor" + // 在某些情况下可能输出 "template constructor" +``` + +解决方案就是给模板构造函数加约束——让它**不要**匹配 `Wrapper` 自身的类型。 + +--- + +## Concept 基础语法 + +C++20 引入了 Concepts——一种命名约束的机制。你可以把 concept 想象成"带名字的编译期布尔条件"。这样说如果你感觉不好懂了——笔者认为,concept这个东西字如其名:就是概念的意思,相比之前我们要用enable_if来晦涩的表达是什么,我们可以更加容易的说出他是什么了——他是XXX,XXX就是一个concept。就这么简单。 + +### 声明 concept + +```cpp +template +concept Integral = std::is_integral_v; +``` + +`Integral` 是一个 concept,它检查 `T` 是否是整数类型。`std::is_integral_v` 是一个编译期布尔常量。我们这里表达的意思很简单——我们就只要一个整形!拿着这个概念,就能下一步的被requires使用了。 + +### 使用 requires 子句 + +`requires` 子句可以加在模板声明后面,用来约束模板参数必须满足某个条件: + +```cpp +template + requires Integral +void foo(T x) { + // 只有 T 是整数类型时,这个函数才会被实例化 +} + +foo(42); // OK:int 是整数 +foo(3.14); // 编译错误:double 不满足 Integral +``` + +### 标准库常用 concept + +C++20 在 `` 头文件中提供了一批预定义的 concept: + +```cpp +#include + +// std::invocable:F 是否可以用 Args... 调用 +static_assert(std::invocable); + +// std::same_as:A 和 B 是否是同一类型 +static_assert(std::same_as); + +// std::convertible_to:From 是否能隐式转换到 To +static_assert(std::convertible_to); +``` + +--- + +## not_the_same_t:逐行拆解 + +现在我们来看 OnceCallback 中的这个 concept: + +```cpp +template +concept not_the_same_t = !std::is_same_v, T>; +``` + +它做的事情用一句话说就是:**F 退化后的类型不是 T**。我们逐个拆解里面的三个关键组件。 + +### std::decay_t:退化掉引用和 cv 限定符 + +`std::decay_t` 对类型做三件事:去掉引用(`int&` → `int`)、去掉顶层 const/volatile(`const int` → `int`)、数组和函数类型退化(`int[5]` → `int*`,`int(int)` → `int(*)(int)`)。 + +在 OnceCallback 的场景里,最关键的是去掉引用。当我们写 `OnceCallback cb2 = std::move(cb1)` 时,`Functor` 被推导为 `OnceCallback`(不是 `OnceCallback&&`,因为转发引用的推导规则会把右值推导为非引用类型)。但如果是 `OnceCallback cb2 = cb1;`(虽然拷贝被删除了,这里只是举例),`Functor` 就会被推导为 `OnceCallback&`。`std::decay_t` 保证了无论 `Functor` 推导出什么引用形式,退化后都是 `OnceCallback`,和 `T = OnceCallback` 做比较。 + +### std::is_same_v<...>:比较两个类型 + +`std::is_same_v` 在 `A` 和 `B` 完全相同时返回 `true`。注意"完全相同"是很严格的——`int` 和 `const int` 不同,`int&` 和 `int` 也不同。这就是为什么我们需要 `std::decay_t` 先统一形式。 + +### 取反 `!`:F 不是 T 时约束通过 + +整个 concept 的值是 `!std::is_same_v, T>`——取反意味着当 `F` 退化后和 `T` 相同时约束失败(模板被排除),不同时约束通过(模板参与重载决议)。 + +### 加上约束后的效果 + +```cpp +template + requires not_the_same_t +explicit OnceCallback(Functor&& f) : status_(Status::kValid), func_(std::move(f)) {} +``` + +当传入的是 `OnceCallback` 本身时(比如移动构造的场景),`not_the_same_t` 求值为 `!true = false`,约束不满足,模板被排除出候选列表,编译器只能选择移动构造函数。当传入的是 lambda、函数指针等其他类型时,约束满足,模板正常参与重载决议,被选为构造函数。 + +--- + +## 这个模式在标准库中的应用 + +这不仅仅是 OnceCallback 的特殊需求。`std::move_only_function` 自己的实现里也有几乎一样的约束——只不过标准库用的是标准 concept `std::constructible_from` 配合 `!std::is_same_v` 的形式。任何 move-only 的类型擦除包装器都需要这个防御——只要你的类同时有"接受任意类型的模板构造函数"和"编译器生成的移动构造函数",就必须加约束来防止两者竞争。 + +```text +模式总结: +模板构造函数 + requires 排除自身类型 = 保护移动语义的正确匹配 +``` + +如果你以后写类似的组件——比如自己的 `unique_function`、`any_invocable` 之类的 move-only 包装器——记住这个模式,它是一个通用的防御手段。 + +--- + +## 踩坑预警 + +### 如果忘记 std::decay_t + +如果只写 `!std::is_same_v` 而不加 `std::decay_t`,问题出在 `F` 的推导结果可能带引用也可能不带引用,取决于调用上下文。考虑以下场景: + +```cpp +OnceCallback cb1([](int x) { return x; }); + +// 场景 A:std::move(cb1) 是右值 +// Functor 推导为 OnceCallback(不带引用) +// is_same_v == true → 约束失败 ✓ 正确 + +// 场景 B:const OnceCallback& ref = cb1; +// 如果有人写了 OnceCallback cb2(ref); +// Functor 推导为 const OnceCallback& +// is_same_v == false → 约束通过 ✗ 错误! +``` + +场景 B 中,不加 `decay_t` 的话,`const OnceCallback&` 和 `OnceCallback` 不相同,约束通过,模板构造函数被选中——但语义上我们期望的是编译错误(拷贝已删除)或至少不是模板构造函数。加了 `decay_t` 后,`const OnceCallback&` 退化为 `OnceCallback`,和 `OnceCallback` 相同,约束正确失败。 + +### static_assert(false) 的陷阱 + +在 C++23 之前,`static_assert(false, "...")` 在模板中会导致所有实例化都触发断言失败——即使这个模板从未被调用。这是因为 C++ 标准在 C++23 之前要求 `static_assert(false)` 在模板定义时就立即求值。Chromium 用 `static_assert(!sizeof(*this), "...")` 来绕过这个限制(`!sizeof` 总是 `false`,但依赖 `*this` 的类型所以是依赖型表达式,不会在定义时求值)。C++23 放宽了这个规则,但如果你用 C++20 编译,仍然需要注意这个问题。 + +--- + +## 小结 + +这一篇我们搞清楚了 OnceCallback 构造函数上那个看似多余的 `requires not_the_same_t` 约束。它的存在是为了防止模板构造函数在 `OnceCallback cb2 = std::move(cb1)` 这种场景下劫持移动构造函数的调用。`not_the_same_t` 通过 `std::decay_t` 去掉 `F` 上的引用和 const 修饰后与 `T` 比较,取反后确保传入自身类型时模板被排除。这个模式在所有 move-only 的类型擦除包装器中都会用到——`std::move_only_function` 也有类似的约束。 + +下一篇我们去看 `std::move_only_function`——它是 OnceCallback 的核心存储类型,也是我们用标准库设施替代 Chromium 手写 BindState 的关键。 + +## 参考资源 + +- [cppreference: Constraints and concepts](https://en.cppreference.com/w/cpp/language/constraints) +- [cppreference: std::decay](https://en.cppreference.com/w/cpp/types/decay) +- [Stack Overflow: Generic constructor template called instead of copy/move constructor](https://stackoverflow.com/questions/70267685/generic-constructor-template-called-instead-of-copy-move-constructor) diff --git a/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-05-once-callback-move-only-function.md b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-05-once-callback-move-only-function.md new file mode 100644 index 000000000..1f7f17548 --- /dev/null +++ b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-05-once-callback-move-only-function.md @@ -0,0 +1,247 @@ +--- +title: "OnceCallback 前置知识(五):std::move_only_function (C++23)" +description: "深入理解 C++23 的 std::move_only_function——OnceCallback 的核心存储类型,从 std::function 的演进动机到 SBO 行为,再到为什么 OnceCallback 需要独立的三态管理" +chapter: 0 +order: 5 +tags: + - host + - cpp-modern + - intermediate + - 函数对象 + - 智能指针 +difficulty: intermediate +platform: host +cpp_standard: [23] +reading_time_minutes: 10 +prerequisites: + - "OnceCallback 前置知识速查:C++11/14/17 核心特性回顾" + - "OnceCallback 前置知识(一):函数类型与模板偏特化" +related: + - "OnceCallback 实战(二):核心骨架搭建" + - "OnceCallback 实战(六):测试与性能对比" +--- + +# OnceCallback 前置知识(五):std::move_only_function (C++23) + +## 引言 + +`std::move_only_function` 是 OnceCallback 的心脏——它承担了所有类型擦除的脏活累活。OnceCallback 的 `func_` 成员就是 `std::move_only_function` 类型,它把 lambda、函数指针、仿函数等各种形态的可调用对象统一包装成同一个已知签名的调用接口。 + +这一篇我们要搞清楚三件事:`std::move_only_function` 和 `std::function` 到底有什么区别、它的 SBO(Small Buffer Optimization)行为是怎么工作的、以及为什么 OnceCallback 不能直接依赖它的判空机制而需要自己搞一套三态管理。 + +> **学习目标** +> +> - 理解 `std::move_only_function` 的设计动机——为什么 `std::function` 不够用 +> - 掌握构造、移动、调用、判空四个核心操作 +> - 理解 SBO 的原理和 `std::move_only_function` 的分配行为 +> - 明白为什么 OnceCallback 需要独立的 `Status` 枚举 + +--- + +## 从 std::function 到 std::move_only_function + +### std::function 的局限 + +`std::function` 是 C++11 引入的通用可调用对象容器,它通过类型擦除把各种可调用对象统一成同一个接口。但 `std::function` 有一个根本性的限制:它要求存储的可调用对象**必须可拷贝**。 + +原因在于 `std::function` 自身是可拷贝的——当你拷贝一个 `std::function` 时,它需要把内部存储的可调用对象也拷贝一份。如果你试图用一个捕获了 `std::unique_ptr` 的 lambda 来构造 `std::function`,编译器会在拷贝语义上直接报错: + +```cpp +#include +#include + +auto ptr = std::make_unique(42); + +// 编译错误!unique_ptr 不可拷贝,std::function 要求可拷贝 +std::function f = [p = std::move(ptr)]() { return *p; }; +``` + +这个限制在 OnceCallback 的场景里是致命的——OnceCallback 的核心卖点就是 move-only,它必须支持捕获 `unique_ptr` 的 lambda。 + +### std::move_only_function 的解决方案 + +`std::move_only_function`(C++23,定义在 `` 中)就是"move-only 版本的 `std::function`"。它删除了拷贝操作,只保留移动操作,从而不再要求存储的可调用对象可拷贝。 + +```cpp +#include +#include + +auto ptr = std::make_unique(42); + +// OK!move_only_function 不要求可拷贝 +std::move_only_function f = [p = std::move(ptr)]() { return *p; }; + +int result = f(); // result == 42 +``` + +两个类型在接口上的关键区别可以概括为:`std::function` 可拷贝可移动,要求存储对象可拷贝;`std::move_only_function` 不可拷贝只可移动,只要求存储对象可移动。 + +--- + +## 四个核心操作 + +### 构造:从可调用对象创建 + +`std::move_only_function` 接受任何匹配签名 `R(Args...)` 的可调用对象——lambda、函数指针、仿函数,甚至另一个 `std::move_only_function`: + +```cpp +// 从 lambda 构造 +std::move_only_function f1 = [](int a, int b) { return a + b; }; + +// 从函数指针构造 +int add(int a, int b) { return a + b; } +std::move_only_function f2 = &add; + +// 从仿函数构造 +struct Multiplier { + int operator()(int a, int b) { return a * b; } +}; +std::move_only_function f3 = Multiplier{}; + +// 默认构造:创建空的 move_only_function +std::move_only_function f4; // f4 == nullptr +``` + +### 移动:转移所有权 + +移动操作把源对象的可调用对象转移到目标对象。移动之后,源对象的状态是**未指定的**——标准没有保证它一定为空。 + +```cpp +std::move_only_function f = []() { return 42; }; +auto g = std::move(f); +// f 的状态未指定——可能为空,也可能不为空 +// 不要依赖 f 在移动后的行为 +``` + +这一点非常重要——也是 OnceCallback 需要自己的 `Status` 枚举的原因之一。我们后面会展开讲。 + +### 调用:通过 operator() 执行 + +调用语法和 `std::function` 一样——直接用 `()` 运算符: + +```cpp +std::move_only_function f = [](int a, int b) { return a + b; }; +int result = f(3, 4); // result == 7 +``` + +如果 `f` 为空(通过默认构造或 `= nullptr`),调用会抛出 `std::bad_function_call` 异常。 + +### 判空:检查是否持有可调用对象 + +通过 `operator bool()` 或与 `nullptr` 比较: + +```cpp +std::move_only_function f; +if (!f) { + std::cout << "f is empty\n"; +} +// 等价于 +if (f == nullptr) { + std::cout << "f is empty\n"; +} + +f = []() { return 42; }; +if (f) { + std::cout << "f is not empty\n"; +} +``` + +也可以通过赋值 `nullptr` 来主动清空: + +```cpp +f = nullptr; // 清空 f,析构之前持有的可调用对象 +``` + +--- + +## SBO:小对象优化 + +### 什么是 SBO + +`std::move_only_function`(和 `std::function` 一样)内部实现了**小对象优化**(Small Buffer Optimization,SBO)。思路很简单:对象内部预留一块固定大小的缓冲区(通常是几个指针大小),如果可调用对象足够小,就把它直接存到缓冲区里,避免堆分配;如果太大,就在堆上分配内存来存储。 + +```text +┌──────────────────────────────────┐ +│ std::move_only_function │ +│ ┌──────────────────────────────┐ │ +│ │ 函数指针/虚表指针 │ │ ← 用于类型擦除的调用分派 +│ ├──────────────────────────────┤ │ +│ │ SBO 缓冲区(通常 16-32 字节)│ │ ← 小对象直接存这里 +│ └──────────────────────────────┘ │ +│ 或 │ +│ ┌──────────────────────────────┐ │ +│ │ 堆指针(指向动态分配的对象) │ │ ← 大对象存在堆上 +│ └──────────────────────────────┘ │ +└──────────────────────────────────┘ +``` + +SBO 的阈值是实现定义的——通常在 2-3 个指针大小(16-24 字节)左右。捕获少量参数的 lambda(比如 `[x = 42]` 或 `[&ref]`)通常能放进 SBO,不会触发堆分配。但如果 lambda 捕获了大量数据(比如一个 `std::string` + 几个 `int`),超过了 SBO 阈值,构造时就会在堆上分配。 + +### sizeof 对比 + +```cpp +#include +#include + +int main() { + std::cout << "sizeof(std::function): " + << sizeof(std::function) << "\n"; + std::cout << "sizeof(std::move_only_function): " + << sizeof(std::move_only_function) << "\n"; +} +``` + +在 GCC 上,典型值是 `std::function` 约 32 字节,`std::move_only_function` 也约 32 字节。两者大小差不多,因为它们使用类似的 SBO 策略。 + +--- + +## 为什么 OnceCallback 需要独立的 Status 枚举 + +你可能已经注意到了一个细节——OnceCallback 在 `std::move_only_function` 之外又加了一个自己的 `Status` 枚举来追踪状态。为什么不直接用 `std::move_only_function` 的判空机制? + +原因是 `std::move_only_function` 的判空无法区分三种不同的状态: + +```cpp +enum class Status : uint8_t { + kEmpty, // 从未被赋值(默认构造) + kValid, // 持有有效的可调用对象 + kConsumed // 已被 run() 调用过 +}; +``` + +`std::move_only_function` 的 `operator bool()` 只能区分"空"和"非空"两种状态。但 OnceCallback 需要知道一个回调是"从来没被赋过值"(kEmpty)还是"曾经有值但已经被调用了"(kConsumed)。这两种情况在调试时的含义完全不同——kEmpty 意味着"你忘了给回调赋值",kConsumed 意味着"回调已经被正确调用了,你不应该再使用它"。 + +还有一个更微妙的问题:`std::move_only_function` 移动后的状态是**未指定的**——标准不保证移动后源对象的 `operator bool()` 返回 `false`。某些实现可能仍然返回 `true`,只是内部数据已经无效了。如果 OnceCallback 依赖 `std::move_only_function` 的判空来判断状态,在移动操作之后可能会得到错误的结果。独立的 `Status` 枚举完全由我们控制——移动构造函数显式把源对象设为 `kEmpty`,不存在歧义。 + +--- + +## 与 Chromium BindState 的对比 + +Chromium 没有使用标准库的类型擦除设施——它手写了一套 `BindState` 系统。对比一下两种方案的核心差异。 + +Chromium 的 `BindState` 是一个堆分配的对象,存储了可调用对象和所有绑定参数。`OnceCallback` 本身只持有一个指向 `BindState` 的智能指针(`scoped_refptr`),大小只有 8 字节——一个指针。所有状态都放在堆上的 `BindState` 里,回调对象本身只是一个"瘦代理"。 + +我们的方案用 `std::move_only_function` 替代了整个 `BindState` 层——它内部实现了类型擦除和 SBO,省去了我们手写函数指针表、SBO 缓冲区、移动/析构操作的工作。代价是对象大小从 8 字节膨胀到约 32 字节(`std::move_only_function` 本身的大小),再加上 `Status` 枚举和可选的 `CancelableToken` 指针,整个 `OnceCallback` 大约 56-64 字节。 + +| 指标 | Chromium BindState | 我们的 std::move_only_function | +|------|-------------------|-------------------------------| +| 回调对象大小 | 8 字节(一个指针) | 56-64 字节 | +| 堆分配 | 总是(new BindState) | 仅当 lambda 超过 SBO 阈值 | +| 移动代价 | 复制一个指针 | 复制 32+ 字节 | +| 实现复杂度 | 很高(手写引用计数+函数指针表) | 低(复用标准库) | + +对于教学目的和大多数实际场景,56-64 字节的回调对象完全不是瓶颈。如果你的项目确实需要极致紧凑,可以参考 Chromium 的方案——核心思路我们在后续实战篇里会讲清楚。 + +--- + +## 小结 + +这一篇我们搞清楚了 `std::move_only_function` 的来龙去脉。它是 C++23 引入的 move-only 版本的 `std::function`,删除了拷贝操作以支持 move-only 的可调用对象。内部实现了 SBO 来优化小对象的存储。但它的移动后状态未指定,且只能区分"空"和"非空"两种状态——这就是 OnceCallback 需要独立的三态 `Status` 枚举的原因。与 Chromium 手写的 `BindState` 相比,我们用对象大小的膨胀换来了实现简洁性的大幅提升。 + +下一篇我们去看 OnceCallback 的最后一个前置知识点——C++23 的 deducing this(显式对象参数),它是 `run()` 方法实现编译期左值/右值拦截的核心机制。 + +## 参考资源 + +- [cppreference: std::move_only_function](https://en.cppreference.com/w/cpp/utility/functional/move_only_function) +- [P0288R9 - move_only_function 提案](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p0288r9.html) +- [cppreference: std::function](https://en.cppreference.com/w/cpp/utility/functional/function) diff --git a/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-06-once-callback-deducing-this.md b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-06-once-callback-deducing-this.md new file mode 100644 index 000000000..64898bc70 --- /dev/null +++ b/documents/vol9-open-source-project-learn/chrome/01_once_callback/full/pre-06-once-callback-deducing-this.md @@ -0,0 +1,225 @@ +--- +title: "OnceCallback 前置知识(六):Deducing this (C++23)" +description: "深入理解 C++23 显式对象参数(deducing this)如何让 OnceCallback::run() 在编译期优雅地拦截左值调用,替代 Chromium 的双重重载 hack" +chapter: 0 +order: 6 +tags: + - host + - cpp-modern + - intermediate + - 模板 +difficulty: intermediate +platform: host +cpp_standard: [23] +reading_time_minutes: 10 +prerequisites: + - "OnceCallback 前置知识速查:C++11/14/17 核心特性回顾" +related: + - "OnceCallback 实战(二):核心骨架搭建" + - "OnceCallback 前置知识(四):Concepts 与 requires 约束" +--- + +# OnceCallback 前置知识(六):Deducing this (C++23) + +## 引言 + +OnceCallback 的 `run()` 方法是整个组件的灵魂,也是 C++23 特性最密集的一个方法。它的声明长这样: + +```cpp +template +auto run(this Self&& self, FuncArgs&&... args) -> ReturnType; +``` + +如果你没见过 `this Self&& self` 这种写法——别慌,这一篇就是专门讲它的。这是 C++23 引入的"显式对象参数"特性,官方名称叫 **deducing this**。它让 OnceCallback 用一个函数模板就实现了"左值调用编译报错、右值调用正常执行"的效果,比 Chromium 的方案干净得多。 + +> **学习目标** +> +> - 理解 deducing this 的语法和推导规则 +> - 掌握 `run()` 如何利用它实现编译期左值/右值拦截 +> - 理解惰性实例化(lazy instantiation)在 `static_assert` 中的作用 +> - 对比 deducing this 和传统 ref-qualifier 的适用场景 + +--- + +## 问题:如何让 cb.run() 编译失败 + +OnceCallback 的核心语义是"只能调用一次,而且必须通过右值调用"。用代码表达就是: + +```cpp +OnceCallback cb([](int x) { return x * 2; }); + +cb.run(5); // 应该编译失败:cb 是左值 +std::move(cb).run(5); // 应该编译通过:std::move(cb) 是右值 +``` + +我们需要一种机制,让 `run()` 能够在编译期区分"通过左值调用"和"通过右值调用",并且对左值调用给出清晰的错误信息。 + +### Chromium 的旧方案 + +Chromium 没有享受 C++23 的福利,它用了一个比较 hack 的方案——两个重载: + +```cpp +// 右值版本:真正的执行 +R Run() && { + // 执行回调... +} + +// 左值版本:编译报错 +R Run() const& { + static_assert(!sizeof(*this), + "OnceCallback::Run() may only be invoked on a non-const rvalue, " + "i.e. std::move(callback).Run()."); +} +``` + +为什么用 `!sizeof(*this)` 而不是直接写 `false`?因为在 C++23 之前,`static_assert(false, "...")` 在模板中会导致所有代码路径都触发断言——即使这个函数从未被调用。C++23 放宽了这个限制。`!sizeof(*this)` 利用了 `sizeof` 必须在完整类型上才能求值的特性——它是一个依赖型表达式,只有在模板实例化时才求值,从而实现了"只在实际调用时才触发"的效果。 + +能工作,但确实不优雅——需要两个重载函数来处理同一件事,而且 `!sizeof` hack 的可读性不好。 + +--- + +## deducing this 的语法与推导规则 + +C++23 的 deducing this 让我们可以把 `this` 显式地写成成员函数的第一个参数,并用模板参数来推导它的类型和值类别。 + +### 基本语法 + +```cpp +struct MyStruct { + void f(this auto&& self) { + // self 就是 this——但它的类型是推导出来的 + } +}; +``` + +`this auto&& self` 是显式对象参数的声明。关键字 `this` 出现在类型前面,告诉编译器"这不是一个普通参数,而是显式的对象参数"。`auto&&` 是推导占位符——编译器会根据调用时对象的值类别来推导 `self` 的具体类型。 + +### 推导规则 + +`self` 的类型推导规则和转发引用(forwarding reference)完全一样——因为 `self` 的推导上下文等效于模板参数: + +- **左值调用** `obj.f()`:`self` 的类型推导为 `MyStruct&`(左值引用) +- **右值调用** `std::move(obj).f()` 或 `MyStruct{}.f()`:`self` 的类型推导为 `MyStruct`(非引用,纯类型) +- **const 左值调用** `std::as_const(obj).f()`:`self` 的类型推导为 `const MyStruct&` + +### 验证推导结果 + +```cpp +#include +#include + +struct Check { + void test(this auto&& self) { + using Self = decltype(self); + if constexpr (std::is_lvalue_reference_v) { + std::cout << "lvalue reference\n"; + } else { + std::cout << "rvalue (not a reference)\n"; + } + } +}; + +int main() { + Check c; + c.test(); // 输出:lvalue reference + std::move(c).test(); // 输出:rvalue (not a reference) + std::as_const(c).test(); // 输出:lvalue reference (const) +} +``` + +--- + +## 在 OnceCallback::run() 中的应用 + +现在我们来看 `run()` 的完整实现,理解它是如何利用 deducing this 来拦截左值调用的。 + +```cpp +template +auto run(this Self&& self, FuncArgs&&... args) -> ReturnType { + static_assert(!std::is_lvalue_reference_v, + "OnceCallback::run() must be called on an rvalue. " + "Use std::move(cb).run(...) instead."); + return std::forward(self).impl_run(std::forward(args)...); +} +``` + +这段代码做了三件事,我们逐一拆解。 + +### 拦截左值调用 + +`std::is_lvalue_reference_v` 检查 `Self` 是否是左值引用类型。当调用方写 `cb.run(args)` 时,`cb` 是左值,`Self` 被推导为 `OnceCallback&`——这是一个左值引用类型,`is_lvalue_reference_v` 返回 `true`,取反后为 `false`,`static_assert` 失败,编译器报出我们写的那句错误信息:"OnceCallback::run() must be called on an rvalue. Use std::move(cb).run(...) instead." + +当调用方写 `std::move(cb).run(args)` 时,`std::move(cb)` 是右值(严格说是 xvalue),`Self` 被推导为 `OnceCallback`——不是引用类型,`is_lvalue_reference_v` 返回 `false`,取反后为 `true`,`static_assert` 通过,代码继续执行。 + +### 转发到 impl_run + +`std::forward(self)` 根据 `Self` 的类型决定是返回左值引用还是右值引用。由于 `static_assert` 已经排除了左值的情况,到达这里的 `Self` 一定是非引用类型(右值),所以 `std::forward(self)` 返回的是右值引用——确保 `impl_run` 在右值上被调用。 + +### 惰性实例化(Lazy Instantiation) + +这里有一个值得玩味的细节——`static_assert` 的条件依赖模板参数 `Self`,所以它只有在模板实例化时才求值。这意味着: + +- 如果 `run()` 从未被调用,`static_assert` 不会触发——不管 `OnceCallback` 对象本身是左值还是右值 +- 只有在某个具体的调用点上,编译器需要实例化这个模板时,`Self` 的具体类型才会被确定,`static_assert` 才会求值 + +这叫"惰性实例化"(lazy instantiation),是 C++ 模板的一个基本特性。函数模板只有在使用时才会被实例化——不使用就不实例化,也不做任何检查。这就是为什么 Chromium 不得不用 `!sizeof(*this)` 而不是直接写 `false`——在 C++23 之前,`static_assert(false)` 不依赖模板参数,会在模板定义时就触发,而不是等实例化时才触发。 + +--- + +## 与传统 ref-qualifier 的对比 + +OnceCallback 里有两个方法表达了"只能通过右值调用"的语义——`run()` 用 deducing this,`then()` 用传统的 ref-qualifier `&&`。为什么不统一用一种方式? + +### then() 用 ref-qualifier + +```cpp +template +auto then(Next&& next) && -> OnceCallback<...>; +``` + +`then()` 的需求很简单——它只接受右值,不接受左值,不需要区分后给出不同的错误信息。如果调用方写了 `cb.then(next)`(左值调用),编译器直接报"没有匹配的重载函数",虽然错误信息不如 deducing this 那么有指导意义,但足够用了。ref-qualifier 写起来也更简洁——一个 `&&` 就完事了。 + +### run() 用 deducing this + +`run()` 的需求更精细——它不仅需要拒绝左值调用,还需要给出一个**有指导意义的错误信息**,告诉调用方"你应该用 `std::move(cb).run(...)` 而不是 `cb.run(...)`"。deducing this 让这个需求变得自然——`static_assert` 可以输出我们自定义的错误信息,而不是编译器默认的"no matching function"。 + +### 选择策略 + +总结一下:如果你只需要"只接受右值"的约束,用 `&&` 限定更简洁。如果你还需要对左值调用给出自定义的错误信息,用 deducing this 配合 `static_assert` 更合适。 + +--- + +## 踩坑预警 + +### 显式对象参数不能与 cv-qualifier 或 ref-qualifier 共存 + +有显式对象参数的成员函数不能同时声明为 `const`、`volatile` 或带 ref-qualifier(`&`/`&&`)。这是因为显式对象参数已经接管了对象类型和值类别的推导——`const` 和 `&&` 限定变得多余甚至矛盾。 + +```cpp +struct Bad { + void f(this auto&& self) const; // 编译错误:不能同时有显式对象参数和 const + void g(this auto&& self) &&; // 编译错误:不能同时有显式对象参数和 && +}; +``` + +### 显式对象参数不能是静态函数 + +显式对象参数函数不是静态函数——它仍然需要一个对象实例来调用。`this` 参数是由编译器从调用表达式推导出来的,不是由调用方手动传入的。 + +### 编译器支持 + +Deducing this 是 C++23 特性。GCC 14+、Clang 18+、MSVC 19.34+ 支持此特性。如果你的编译器不支持,只能回退到 Chromium 的双重重载方案。 + +--- + +## 小结 + +这一篇我们搞清楚了 deducing this 的来龙去脉。它让 `run()` 用一个函数模板就实现了编译期的左值/右值拦截——通过 `Self` 的推导类型判断调用方传的是左值还是右值,配合 `static_assert` 给出有指导意义的错误信息。相比 Chromium 的两个重载 + `!sizeof` hack,deducing this 方案更简洁、更符合 C++ 的设计哲学。而 `then()` 不需要自定义错误信息,用传统的 `&&` 限定更简洁。 + +到这里,所有前置知识都讲完了。下一篇我们正式进入 OnceCallback 的实战环节——从动机分析开始,设计我们的目标 API。 + +## 参考资源 + +- [P0847R7 - Deducing this 提案](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0847r7.html) +- [C++23's Deducing this (Microsoft C++ Blog)](https://devblogs.microsoft.com/cppblog/cpp23-deducing-this/) +- [cppreference: Explicit object parameter](https://en.cppreference.com/w/cpp/language/member_functions#Explicit_object_parameter) diff --git a/documents/vol9-open-source-project-learn/chrome/01_once_callback/hands_on/01-once-callback-design.md b/documents/vol9-open-source-project-learn/chrome/01_once_callback/hands_on/01-once-callback-design.md new file mode 100644 index 000000000..353063d7e --- /dev/null +++ b/documents/vol9-open-source-project-learn/chrome/01_once_callback/hands_on/01-once-callback-design.md @@ -0,0 +1,304 @@ +--- +title: "once_callback 设计指南(一):动机与接口设计" +description: "从 Chromium OnceCallback 出发,设计一个 C++23 的 move-only、一次性消费回调组件——第一部分聚焦动机分析和 API 设计" +chapter: 1 +order: 1 +tags: + - host + - cpp-modern + - advanced + - 回调机制 + - 函数对象 +difficulty: advanced +platform: host +cpp_standard: [23] +reading_time_minutes: 20 +prerequisites: + - "std::function、std::invoke 与可调用对象" + - "移动语义与完美转发" +related: + - "OnceCallback 与 RepeatingCallback" + - "bind_once / bind_repeating 与参数绑定" +--- + +# once_callback 设计指南(一):动机与接口设计 + +## 引言 + +说实话,笔者在做异步编程的时候,踩过最多的坑就是回调被多次调用。场景很经典:注册一个文件 I/O 完成的回调,期望它跑一次就完事,结果因为某处逻辑手滑多触发了一次,回调里释放的资源被二次访问,直接喜提段错误。这种 bug 的一大特点是——在测试里很难复现,因为正常的异步路径往往只跑一次回调;真正的触发条件是某种竞态或错误重试路径。 + +`std::function` 没法帮我们。它允许多次调用,允许拷贝传播,回调对象可以满天飞。我们在卷二已经拆解过 `std::function` 的内部机制(类型擦除 + SBO)和它的 `LightCallback` 简化实现——那个版本解决了类型擦除的开销问题,但完全没有触及"回调应该被调用几次"这个语义问题。 + +Chromium 团队在设计 `base::OnceCallback` 的时候,给出了一个非常漂亮的回答:**让回调的类型系统本身来约束调用语义**。`OnceCallback` 是 move-only 类型,它的 `Run()` 方法只能通过右值引用调用(`std::move(cb).Run()`),调用一次之后回调对象就被消费掉了,再调就是空操作或者断言失败。这个设计在 Chrome 浏览器每天数百亿次的任务投递中经过了充分验证。 + +我们这一系列的目标不是照搬 Chromium 的实现(那个实现非常复杂,涉及手写的引用计数、`TRIVIAL_ABI` 注解、函数指针分派表),而是利用 C++23 的新特性——特别是 `std::move_only_function` 和 deducing this——来实现一个保留了 Chromium 设计精髓、但代码量可控的 `OnceCallback` 组件。 + +> **学习目标** +> +> - 理解"move-only + 一次性消费"为什么是回调的正确语义约束 +> - 设计 `OnceCallback` 的完整公共接口 +> - 分析 Chromium `OnceCallback` 的内部架构,理解每个设计决策背后的原因 + +--- + +## 我们的问题:`std::function` 在异步场景的三大缺陷 + +在动手设计之前,我们先把问题拆清楚。`std::function` 作为通用的可调用对象容器,在设计上是成功的——但在异步回调这个特定场景下,它有三个让笔者血压拉满的问题。 + +**第一,可复制。** `std::function` 天生支持拷贝,这意味着一个回调可以被复制到任意多个地方。在异步系统中,这等于允许多个执行路径同时持有同一份回调的副本。如果回调里捕获了 move-only 的资源(比如 `std::unique_ptr`),拷贝直接编译失败;如果捕获的是裸指针或引用,多个副本同时执行就会产生竞态。Chrome 团队的思路很直接:既然异步任务回调从根本上就不应该被复制,那就让它在类型层面不可拷贝。 + +**第二,可重复调用。** `std::function::operator()` 对调用次数没有任何约束。你可以在同一个 `std::function` 上调一千次,它照跑不误。但在异步回调场景里,一个文件读取完成的回调被调用两次就是逻辑错误——它可能触发两次资源释放、两次状态转换、两次消息发送。这种错误在类型系统里完全检测不到,只能靠运行时的断言(如果有的话)或者——更常见的情况——靠 bug 现场来发现。 + +**第三,无法表达消费语义。** 在 Chrome 的任务投递模型中,一个 `PostTask(FROM_HERE, callback)` 调用之后,`callback` 就不应该再被使用——它的所有权已经转移给了任务系统。`std::function` 的 `operator()` 是 `const` 限定的,调用它不会改变 `std::function` 对象本身的状态,所以你无法通过调用接口来表达"调用即消费"这个语义。 + +这三个问题归结到一点:`std::function` 的接口设计无法表达"这个回调只能被调用一次,调用后即失效"这个约束。Chrome 的 `OnceCallback` 正是为了填补这个语义空白而设计的。 + +--- + +## Chromium 的回答:`OnceCallback` 设计哲学 + +Chrome 的回调系统建立在一条核心原则之上:**消息传递优于锁,序列化优于线程**。在这个原则下,每个投递到任务系统的回调(Chrome 里叫 task)都是一个独立的、一次性的消息。投递之后,回调的所有权就从调用方转移到了任务系统;执行之后,回调就被销毁。没有共享,没有复用,没有歧义。 + +这个哲学直接体现在 `OnceCallback` 的类型设计上: + +- **Move-only**:`OnceCallback` 删除了拷贝构造和拷贝赋值,只保留移动操作。这从类型层面保证了回调在任意时刻只有一个持有者。 +- **右值限定 `Run()`**:`OnceCallback::Run()` 只能通过右值引用调用(`std::move(cb).Run(args...)`)。左值调用会触发 `static_assert`,产生一条明确的编译错误。这从语法层面提醒调用方:"你在消费这个回调,之后别再用了。" +- **单次消费**:`Run()` 内部会通过引用计数机制销毁 `BindState`,使得后续对同一对象的任何访问都是安全的空操作。 + +Chrome 实际上还有 `RepeatingCallback`——一个可复制的、可重复调用的版本。两个回调类共享同一套 `BindState` 内部实现,区别仅在于 `Run()` 的值类别限定和 `BindState` 的所有权语义。这种设计允许同一套绑定基础设施同时服务于"一次性任务"和"重复监听器"两种截然不同的使用模式。 + +### Chromium 内部实现概览 + +我们不用深入 Chromium 的每一行源码,但需要理解它的核心架构,因为我们的 `OnceCallback` 会借鉴同样的分层思路,只是用 C++23 的标准设施来简化实现。 + +Chromium 的回调系统由三个层次组成,从底到顶依次是: + +**底层:`BindStateBase`**——类型擦除的基类。它带引用计数,但有趣的是,它**不使用虚函数**。取而代之的是三个函数指针成员:`polymorphic_invoke_`(负责调用)、`destructor_`(负责析构)、`query_cancellation_traits_`(负责取消查询)。Chrome 团队选择函数指针而非虚函数的原因是减少二进制文件膨胀。虚函数会为每个模板实例化生成一个独立的 vtable(虚函数表),如果一个项目里有 100 种不同的 `BindState` 实例化,就会有 100 个 vtable。而函数指针的方式可以复用同一份静态函数,只有指向函数的指针值不同,不会产生额外的代码段。 + +**中间层:`BindState`**——模板化的具体类,继承自 `BindStateBase`。它存储了真正的可调用对象(`Functor`)和通过 `BindOnce` 绑定的参数(`BoundArgs...`)。你可以把它理解为一个"装着所有东西的盒子":盒子里有你的 lambda、绑定的参数、以及基类要求的那些函数指针。这个类的实例通过 `scoped_refptr`(Chromium 自己实现的 intrusive 引用计数智能指针)管理生命周期——`OnceCallback` 在 `Run()` 时释放引用,`RepeatingCallback` 在每次 `Run()` 时保持引用。 + +**顶层:`OnceCallback` 和 `RepeatingCallback`**——用户直接操作的类型。它们本质上是 `BindStateHolder` 的薄包装,而 `BindStateHolder` 只是一个带 `TRIVIAL_ABI` 注解的 `scoped_refptr`。`TRIVIAL_ABI` 是 Clang 的扩展属性,告诉编译器"这个类型可以像 int 一样在寄存器中传递",这使得 `OnceCallback` 的实际大小只有一个指针(8 字节),移动操作仅仅是复制一个指针——极其轻量。 + +这三层之间的关系可以用一句话概括:**顶层回调对象只是一个指向中间层盒子的指针,盒子里装着底层要求的函数指针和真正的数据**。我们接下来设计的 `OnceCallback` 会保留这个"外层接口 + 中间存储 + 类型擦除"的分层思路,但用 `std::move_only_function` 来替代 Chromium 手写的 `BindState` + `scoped_refptr` 组合,用 deducing this 来替代 `const&` 重载 + `static_assert` 的 hack。 + +--- + +## 环境说明 + +先确认一下我们的工具链。`OnceCallback` 依赖以下 C++23 特性: + +- **`std::move_only_function`**(``):C++23 引入的 move-only 类型擦除可调用包装器,是我们的核心构建块 +- **Deducing this**(显式对象参数 `this auto&& self`):C++23 特性,允许在成员函数中推导 `this` 的值类别 +- **`if consteval`**:编译期条件判断(部分实现中可能用到) + +编译器要求方面,GCC 12+ 或 Clang 16+ 可以完整支持上述特性。编译时加 `-std=c++23` 即可。可以用下面这段代码快速验证环境: + +```cpp +#include + +// 验证 std::move_only_function 可用 +static_assert(__cpp_lib_move_only_function >= 202110L); + +// 验证 deducing this 可用(编译通过即说明支持) +struct Check { + void test(this auto&& self) {} +}; + +int main() { + Check c; + c.test(); + return 0; +} +``` + +如果这段代码编译通过,说明环境就绑了。不过说实话,截止笔者写这篇文章时,部分编译器的 `std::move_only_function` 实现还有 bug(比如 GCC 12 的早期版本在某些 SFINAE 场景下会编译失败),建议使用 GCC 13+ 或 Clang 17+ 的最新稳定版本。 + +### 前置知识 + +我们假设读者已经熟悉以下内容(对应的卷二文章已经覆盖): + +- **移动语义与完美转发**:`OnceCallback` 的核心就是 move-only,如果对 `std::move` 和 `std::forward` 的原理不熟,实现过程中会非常痛苦。对应文章:卷二 ch00 移动语义系列。 +- **`std::function` 的类型擦除与 SBO**:我们直接在 `std::move_only_function` 之上构建,需要理解类型擦除的基本原理和小对象优化是什么、为什么重要。对应文章:卷二 ch03 `std::function` 与可调用对象。 +- **`std::invoke` 与统一调用协议**:`bind_once` 内部用 `std::invoke` 来统一处理函数指针、成员函数指针、仿函数等不同类型的可调用对象。对应文章:同上。 +- **可变参数模板与参数包展开**:`OnceCallback` 的模板特化、`bind_once` 的参数绑定都需要熟悉参数包语法。对应文章:卷二 ch00 完美转发、卷四 模板基础。 +- **`std::invoke` 与统一调用协议**:`bind_once` 内部用 `std::invoke` 来统一处理函数指针、成员函数指针、仿函数等不同类型的可调用对象。对应文章:同上。 +- **可变参数模板与参数包展开**:`OnceCallback` 的模板特化、`bind_once` 的参数绑定都需要熟悉参数包语法。对应文章:卷二 ch00 完美转发、卷四 模板基础。 + +--- + +## 设计接口:我们想要什么样的 API + +我们先把目标 API 定下来,再回头讨论每个设计决策。这是工程师的工作方式——先想清楚"我要什么",再想"怎么做"。 + +### 核心用法 + +```cpp +#include "once_callback/once_callback.hpp" + +// 1. 构造:从 lambda 创建 +using namespace tamcpp::chrome; +auto cb = OnceCallback([](int a, int b) { + return a + b; +}); + +// 2. 调用:必须通过右值(std::move) +int result = std::move(cb).run(3, 4); // result == 7 + +// 3. 调用后,cb 被消费 +// std::move(cb).run(1, 2); // 运行时断言失败:callback already consumed +``` + +### 参数绑定 + +```cpp +// bind_once:预绑定部分参数,返回一个 OnceCallback +using namespace tamcpp::chrome; +auto bound = bind_once( + [](int x, int y, int z) { return x + y + z; }, + 10, 20 // 预绑定前两个参数 +); + +int r = std::move(bound).run(30); // r == 60 +``` + +### 取消检查 + +```cpp +using namespace tamcpp::chrome; +auto cb = OnceCallback([](int x) { /* ... */ }); + +// 检查回调是否仍然有效 +if (!cb.is_cancelled()) { + std::move(cb).run(42); +} + +// maybe_valid:乐观检查,适用于跨序列场景 +if (cb.maybe_valid()) { + // "可能"有效,不保证 + std::move(cb).run(42); +} +``` + +### 链式组合 + +```cpp +using namespace tamcpp::chrome; +// then():将当前回调的返回值传给下一个回调 +auto pipeline = OnceCallback([](int a, int b) { + return a + b; +}).then([](int sum) { + return sum * 2; +}); + +int final_result = std::move(pipeline).run(3, 4); +// final_result == 14 (3+4)*2 +``` + +### 接口设计决策分析 + +现在我们逐个讨论这些 API 背后的设计决策。 + +**为什么是 `run()` 而不是 `operator()`?** + +Chromium 用的是 `Run()`(Google C++ 风格要求大写开头)。我们用 `run()` 符合 snake_case 命名规范。但更深层的原因是语义区分:`operator()` 太通用,任何可调用对象都有 `operator()`;`run()` 明确表达了"执行任务"的语义,在代码审查时一眼就能看出这是在消费一个 `OnceCallback`,而不是调用一个普通的可调用对象。 + +**为什么 `run()` 必须通过右值调用?** + +这是整个设计中最关键的一点。我们需要一种机制,让 `cb.run(args)`(左值调用)编译失败,而 `std::move(cb).run(args)`(右值调用)编译通过。Chromium 的实现是通过两个重载来达成的:一个 `Run() &&` 是真正的执行版本,一个 `Run() const&` 内部放了一个 `static_assert(!sizeof(*this))` 来产生编译错误。这个 hack 虽然有效但很丑。 + +我们利用 C++23 的 **deducing this**(显式对象参数)可以做得更优雅。简单来说,deducing this 允许我们在成员函数里把 `this` 显式写成一个模板参数,编译器会根据调用时对象是左值还是右值来推导这个参数的类型。利用这个特性,`run(this auto&& self, Args... args)` 通过推导 `self` 的值类别来区分左值和右值调用,在编译期就拦截非法用法: + +```cpp +template +auto run(this Self&& self, FuncArgs&&... args) -> ReturnType { + static_assert(!std::is_lvalue_reference_v, + "OnceCallback::run() must be called on an rvalue. " + "Use std::move(cb).run(...) instead."); + // ... 实际调用逻辑 +} +``` + +当调用方写 `cb.run(args)` 时,`Self` 被推导为 `OnceCallback&`(左值引用),`static_assert` 触发,报错信息直接告诉调用方该怎么做。当写 `std::move(cb).run(args)` 时,`Self` 被推导为 `OnceCallback`(右值),编译通过。deducing this 的具体工作机制和与 Chromium 方案的详细对比,我们在下一篇的实现篇里会展开讲。 + +**为什么要区分 `is_cancelled()` 和 `maybe_valid()`?** + +这个设计直接来自 Chromium 的 `CancellationQueryMode`。区别在于安全保证的强弱。`is_cancelled()` 提供确定性回答——它只能在回调绑定的序列上调用,保证返回准确的结果。`maybe_valid()` 提供乐观估计——它可以从任何线程调用,但结果可能过时。在实际使用中,`is_cancelled()` 用于"在投递前检查是否还有意义"的判断,`maybe_valid()` 用于"跨线程快速检查是否值得投递"的优化路径。 + +在我们的简化实现中,这两个方法都通过 `CancelableToken` 来查询——`is_cancelled()` 检查状态是否有效以及令牌是否仍然有效,`maybe_valid()` 就是 `!is_cancelled()` 的简单包装。后续如果需要更精细的线程安全语义,可以在这两个方法上做区分。 + +**`then()` 为什么消费 `*this`?** + +`then()` 的语义是"把当前回调的执行结果传给下一个回调"。这要求当前回调在 `then()` 返回的新回调中被完整捕获(capture)。如果 `then()` 不消费 `*this`,就会导致同一个回调同时存在于两个地方——原位置和 `then()` 返回的新回调中——这违反了 move-only 的语义约束。所以 `then()` 被声明为右值限定成员函数(`then(...) &&`),调用后原回调对象进入已消费状态。 + +--- + +## 内部机制:类型擦除的两层架构 + +接口设计好了,我们来看看内部应该怎么组织。Chromium 用了 `BindStateBase` + `scoped_refptr` + 函数指针表这套组合拳来实现类型擦除,效果很好但代码量惊人。我们的策略是用 `std::move_only_function` 来承担类型擦除和小对象优化的脏活累活,把精力集中在消费语义、参数绑定和链式组合这些有趣的部分上。 + +### 为什么选 `std::move_only_function` + +`std::move_only_function` 是 C++23 引入的,它的定位就是"move-only 版本的 `std::function`"。它内部实现了类型擦除和 SBO,行为和 `std::function` 类似,但删除了拷贝操作。 + +你可能已经注意到了 `OnceCallback` 这种写法——`R(Args...)` 看起来像一个函数声明,但在模板参数的上下文中,它是一个**函数类型**(function type)。`int(int, int)` 描述的是"接受两个 int 参数、返回 int 的函数",它是一种合法的 C++ 类型。我们通过模板偏特化来拆解这个类型——下一篇会详细讲解这个技巧。 + +用 `std::move_only_function` 做内部存储有几个好处。它省去了我们手写类型擦除的工作——回想卷二的 `LightCallback`,我们花了一整个章节来手写函数指针表、SBO 缓冲区、移动/析构操作,而 `std::move_only_function` 把这些全部封装好了,直接拿来用。它也天然支持 move-only 的可调用对象——如果我们的回调捕获了 `std::unique_ptr`,`std::function` 会因为拷贝语义的要求直接编译失败,而 `std::move_only_function` 没有这个问题。而且它的 SBO 实现经过了标准库作者的精心调优,在绝大多数情况下不需要堆分配——对于捕获少量参数的 lambda 来说,性能完全够用。 + +### 三态管理 + +引入 `std::move_only_function` 之后,有一个设计问题需要解决:如何区分"空回调"和"已消费回调"? + +`std::move_only_function` 本身可以是空的(默认构造或从 `nullptr` 构造),但"空"和"已被 `run()` 消费过"是两个不同的状态。空回调意味着"从未被赋值过",调用它应该触发一个明确的错误("callback is null")。已消费回调意味着"曾经有值,但已经被调用过了",调用它也应该触发错误("callback already consumed"),但错误信息不同,这对调试很有帮助。 + +所以我们的内部状态需要三态: + +```cpp +enum class Status : uint8_t { + kEmpty, // 默认构造,从未被赋值 + kValid, // 持有有效的可调用对象 + kConsumed // 已被 run() 消费 +}; +``` + +结合 `std::move_only_function`,我们的内部存储结构大致如下: + +```cpp +template +class OnceCallback { + std::move_only_function func_; + Status status_ = Status::kEmpty; + + // 取消令牌(可选) + std::shared_ptr token_; +}; +``` + +移动构造时,`func_` 和 `status_` 一起移动过去,源对象的状态设为 `kEmpty`。`run()` 执行时,先检查 `status_` 是否为 `kValid`,执行完后将 `func_` 置空、`status_` 设为 `kConsumed`。这样在调试时就能根据 `status_` 的值给出精确的错误信息。 + +### 与 Chromium 原版的取舍 + +用 `std::move_only_function` 做底层存储,我们获得了简洁的实现,但也牺牲了一些东西。Chromium 的 `OnceCallback` 大小只有一个指针(8 字节),这得益于 `TRIVIAL_ABI` 注解和引用计数的 `BindState`——回调对象本身只是一个指向堆上 `BindState` 的指针。我们的 `OnceCallback` 包装了 `std::move_only_function`(通常 32 字节)加上 `Status` 枚举和可选的 `CancelableToken` 指针(16 字节),总大小大约在 56-64 字节左右。 + +另一个差异是引用计数。Chromium 的 `BindState` 是引用计数的,允许多个回调共享同一份绑定状态(这对 `RepeatingCallback` 的拷贝语义是必需的)。我们的实现里,`std::move_only_function` 本身是独占所有权的,不支持共享。对于 `OnceCallback` 的 move-only 语义来说这不是问题,但后续实现 `RepeatingCallback` 时需要重新考虑这个设计。 + +这些取舍是合理的——我们用大小和引用计数的灵活性,换来了大幅降低的实现复杂度。在实际使用中,56-64 字节的回调对象在绝大多数场景下都不是瓶颈,而清晰的代码结构让维护和扩展的成本低得多。 + +--- + +## 小结 + +这一篇我们完成了 `once_callback` 的设计基础。核心要点: + +- `std::function` 在异步回调场景有三大缺陷:可复制、可重复调用、无法表达消费语义 +- Chromium 的 `OnceCallback` 通过 move-only + 右值限定 `Run()` + 单次消费来约束回调语义 +- 我们的 `OnceCallback` 用 `std::move_only_function` 做底层类型擦除,用 deducing this 实现右值限定的 `run()` +- 内部采用三态管理(`kEmpty` / `kValid` / `kConsumed`)区分空回调和已消费回调 + +下一篇我们会进入实现阶段:从核心骨架 `run()` 开始,逐步添加 `bind_once`、取消检查和 `then()` 链式组合。 + +## 参考资源 + +- [Chromium Callback 文档](https://chromium.googlesource.com/chromium/src/+/main/docs/callback.md) +- [Chromium callback.h 源码](https://chromium.googlesource.com/chromium/src/+/HEAD/base/functional/callback.h) +- [cppreference: std::move_only_function](https://en.cppreference.com/w/cpp/utility/functional/move_only_function) +- [P0847R7 - Deducing this 提案](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0847r7.html) diff --git a/documents/vol9-open-source-project-learn/chrome/01_once_callback/hands_on/02-once-callback-implementation.md b/documents/vol9-open-source-project-learn/chrome/01_once_callback/hands_on/02-once-callback-implementation.md new file mode 100644 index 000000000..bbddec413 --- /dev/null +++ b/documents/vol9-open-source-project-learn/chrome/01_once_callback/hands_on/02-once-callback-implementation.md @@ -0,0 +1,411 @@ +--- +title: "once_callback 设计指南(二):逐步实现" +description: "从核心骨架到完整组件,四步走读 once_callback 的实现策略,重点理解模板技巧和所有权设计" +chapter: 1 +order: 2 +tags: + - host + - cpp-modern + - advanced + - 回调机制 + - 函数对象 +difficulty: advanced +platform: host +cpp_standard: [23] +reading_time_minutes: 30 +prerequisites: + - "once_callback 设计指南(一):动机与接口设计" +related: + - "bind_once / bind_repeating 与参数绑定" + - "回调取消与组合模式" +--- + +# once_callback 设计指南(二):逐步实现 + +## 引言 + +上一篇我们完成了动机分析和接口设计,确定了 `OnceCallback` 的目标 API 和内部架构。这一篇我们正式上手写代码。不过先说好——这一篇的重点不是"把完整实现端上来",而是带你理解每一步的设计思路和关键技术选型。我们会看到代码的关键骨架,但不会贴出完整的、可直接编译的头文件——那些细节留给课后练习和第三篇的测试验证。 + +实现分四步,每一步都建立在前一步的基础上:先搞定核心的 `run()` 语义,再加参数绑定,然后是取消检查,最后是 `then()` 链式组合。每一步我们只关注"这个组件长什么样"和"关键的模板技巧是什么",不会逐行解读实现。 + +> **学习目标** +> +> - 理解 `OnceCallback` 的模板偏特化模式和内部存储设计 +> - 掌握 deducing this、requires 约束、lambda capture pack expansion 等高级模板技巧在实际组件中的应用 +> - 理解 `bind_once()` 的参数绑定机制和 `then()` 的所有权链设计 + +--- + +## 第一步:核心骨架 — 从模板偏特化开始 + +### 为什么是 `OnceCallback` 这种写法 + +你可能已经注意到了,我们声明 `OnceCallback` 的方式有点特殊——不是 `OnceCallback`,而是 `OnceCallback`。这种写法叫"签名式模板参数"(signature-style template parameter),`std::function` 和 `std::move_only_function` 也是这么做的。 + +背后的技巧是**模板偏特化**(template partial specialization)。我们先声明一个主模板,只有声明没有定义: + +```cpp +template +class OnceCallback; // 主模板:不提供实现 +``` + +然后为 `FuncSignature` 恰好是函数类型的情形提供一个偏特化版本: + +```cpp +template +class OnceCallback { + // 所有真正的代码都在这个偏特化里 +}; +``` + +当用户写 `OnceCallback` 时,编译器把 `int(int, int)` 当作一个整体类型匹配到主模板的 `FuncSignature`,然后发现偏特化版本能将这个整体拆解成返回类型 `ReturnType = int` 和参数包 `FuncArgs... = {int, int}`,于是选择偏特化版本。这个模式的好处是用户可以用一种非常自然的"函数签名"语法来指定回调的类型,而不需要分别传入返回值和参数列表。 + +这里有一个容易混淆的点:`R(Args...)` 看起来像函数声明,但在模板参数的上下文中,它是一个**函数类型**(function type)。`int(int, int)` 是一种合法的 C++ 类型——它描述的是"接受两个 int 参数、返回 int 的函数"。模板偏特化利用了这个类型,通过模式匹配把它拆开,提取出返回值类型和参数包。 + +### 内部存储:类的骨架长什么样 + +上一篇我们确定了三态架构。现在来看看类的骨架——先不管方法实现,只看数据成员和接口签名: + +```cpp +template +class OnceCallback { + // 核心存储:持有实际的可调用对象 + // 不管你传入 lambda、函数指针还是仿函数,它都能装下 + std::move_only_function func_; + + // 三态标记:kEmpty → kValid → kConsumed + Status status_ = Status::kEmpty; + + // 取消令牌(可选) + std::shared_ptr token_; + +public: + // 构造:接受任意可调用对象(带 requires 约束,后面解释) + template + requires not_the_same_t + explicit OnceCallback(Functor&& f); + + // Move-only:删除拷贝 + OnceCallback(const OnceCallback&) = delete; + OnceCallback& operator=(const OnceCallback&) = delete; + OnceCallback(OnceCallback&& other) noexcept; + OnceCallback& operator=(OnceCallback&& other) noexcept; + + // 核心:执行回调并消费 *this(用 deducing this 实现,后面解释) + template + auto run(this Self&& self, FuncArgs&&... args) -> ReturnType; + + // 查询接口 + [[nodiscard]] bool is_cancelled() const noexcept; + [[nodiscard]] bool maybe_valid() const noexcept; + [[nodiscard]] bool is_null() const noexcept; + explicit operator bool() const noexcept; + + // 设置取消令牌 + void set_token(std::shared_ptr token); + + // 链式组合 + template auto then(Next&& next) &&; + +private: + ReturnType impl_run(FuncArgs... args); // 真正的执行逻辑 +}; +``` + +骨架里的每一个成员都有明确的职责。`func_` 负责类型擦除——把各种不同形态的可调用对象统一成一个已知签名的调用接口。`status_` 是一个三态枚举,区分"从未赋值"(kEmpty)、"随时可调用"(kValid)和"已经调用过了"(kConsumed)。`token_` 是一个可选的取消令牌,用于在回调执行前检查是否应该取消执行。移动操作做指针级别的转移,源对象回到 kEmpty 状态。 + +接下来我们聚焦骨架里两个最精巧的部分:`run()` 的 deducing this 技巧和构造函数的 `requires` 约束。这两个是整个组件里模板技巧最密集的地方,值得单独拿出来讲透。 + +### deducing this:让编译器帮我们拦截错误调用 + +`run()` 是整个组件的灵魂,也是 C++23 特性最密集的一个方法。先看它的声明: + +```cpp +template +auto run(this Self&& self, Args... args) -> R; +``` + +如果你没见过 `this Self&& self` 这种写法,别慌,我们一步步来。 + +#### 什么是 deducing this + +deducing this 是 C++23 引入的特性,官方名称叫"显式对象参数"(explicit object parameter)。在传统的成员函数里,`this` 是隐式参数——编译器自动传入当前对象的地址,你看不见也摸不着。deducing this 让我们可以把 `this` 显式地写成函数的第一个参数,并且用模板参数来推导它的类型和值类别。 + +```cpp +// 传统写法:this 是隐式的 +void run(FuncArgs... args); // 编译器看到的是 run(OnceCallback* this, FuncArgs... args) + +// deducing this 写法:this 是显式的 +template +auto run(this Self&& self, FuncArgs&&... args) -> ReturnType; // self 就是 this +``` + +关键在于 `Self&&`——它看起来像右值引用,但实际上是**转发引用**(forwarding reference),因为 `Self` 是模板参数。转发引用的特殊之处在于,它可以根据传入参数的值类别被推导为不同的类型: + +- `cb.run(args)` — `cb` 是左值,`Self` 推导为 `OnceCallback&`(左值引用) +- `std::move(cb).run(args)` — `std::move(cb)` 是右值,`Self` 推导为 `OnceCallback`(纯右值) +- `std::as_const(cb).run(args)` — const 左值,`Self` 推导为 `const OnceCallback&` + +#### 我们怎么利用它 + +知道了 `Self` 的推导规则,拦截左值调用就很简单了: + +```cpp +template +auto run(this Self&& self, FuncArgs&&... args) -> ReturnType { + static_assert(!std::is_lvalue_reference_v, + "OnceCallback::run() must be called on an rvalue. " + "Use std::move(cb).run(...) instead."); + return std::forward(self).impl_run(std::forward(args)...); +} +``` + +`std::is_lvalue_reference_v` 是一个编译期常量,检查 `Self` 是不是左值引用类型。当调用方写 `cb.run(args)` 时,`Self` 被推导为 `OnceCallback&`,这是一个左值引用,条件为 `true`,取反后 `static_assert` 失败,编译器直接报错——报错信息就是我们写的那句话。当调用方写 `std::move(cb).run(args)` 时,`Self` 被推导为 `OnceCallback`,不是引用,`static_assert` 通过,进入 `impl_run` 执行真正的逻辑。注意这里用的是 `std::forward(self)` 而不是 `self.run_impl()`,这确保了 `impl_run` 被正确地在右值上调用。 + +这里有一个值得玩味的细节:`static_assert` 的条件依赖模板参数 `Self`,所以它只有在模板实例化时才求值。这意味着如果 `run()` 从未被调用,`static_assert` 不会触发——不管传的是左值还是右值。只有在某个调用点上编译器需要实例化这个模板时,`Self` 的具体类型才会被确定,`static_assert` 才会求值。这叫"惰性实例化"(lazy instantiation),是模板元编程里非常常见的模式。 + +#### 跟 Chromium 的做法对比 + +Chromium 没有享受 C++23 的福利,它用的是两个重载:`Run() &&` 是真正的执行版本,`Run() const&` 里面放了一个 `static_assert(!sizeof(*this), "...")` 来产生编译错误。`!sizeof` 那个 hack 利用了 C++ 的一个性质:`sizeof` 必须在完整类型上才能求值,所以 `!sizeof(*this)` 求值时一定在类的定义内部(`*this` 的类型是完整的),表达式的值一定是 `false`。在 C++23 之前,直接写 `static_assert(false, "...")` 会在所有代码路径上触发(即使这个重载从未被调用),所以 Chromium 不得不用 `!sizeof` 的技巧。C++23 放宽了这个限制,但 Chromium 的代码库还没有全面迁移到 C++23,所以仍然保留着旧写法。 + +我们的 deducing this 方案只需要一个函数模板,通过 `Self` 的推导自然地区分左值和右值,比 Chromium 的两个重载 + `!sizeof` hack 干净得多。 + +### 构造函数的 requires 约束 + +构造函数模板上有一行看起来多余的约束: + +```cpp +template + requires not_the_same_t +explicit OnceCallback(Functor&& f); +``` + +为什么不直接 `template` 就完事了?问题出在模板构造函数和移动构造函数之间的竞争。 + +当我们写 `OnceCallback cb2 = std::move(cb1)` 时,编译器面前有两条路:调用隐式声明的移动构造函数 `OnceCallback(OnceCallback&&)`,或者把模板构造函数实例化为 `OnceCallback(OnceCallback&&)`(令 `Functor = OnceCallback`)。直觉上我们会觉得移动构造函数是"更特殊"的匹配,应该优先选择。但 C++ 的重载决议规则不是这么运作的——在某些情况下,模板实例化出来的函数签名比隐式声明的特殊成员函数是"更精确"的匹配,编译器会毫不犹豫地选择模板版本。这可能导致意想不到的行为,比如模板构造函数可能不会正确地将源对象的状态设为 kEmpty。 + +我们的实现用了一个自定义 concept `not_the_same_t` 来解决这个问题:`!std::is_same_v, T>` 意味着"当 `F` 的退化类型恰好是 `T` 本身时,排除这个模板"。退化(decay)在这里的作用是去掉 `F` 上的引用和 cv 限定符——因为 `F` 可能是 `OnceCallback&&` 或 `const OnceCallback&`,退化后都变成 `OnceCallback`。加上约束后,当传入的是 `OnceCallback` 本身时模板被排除,编译器才会正确地匹配移动构造函数。 + +这个技巧在实现 move-only 的类型擦除包装器时非常常见——`std::move_only_function` 自己的实现里也有类似的约束。如果你以后写类似的组件,记住这个模式:**模板构造函数 + requires 排除自身类型 = 保护移动语义的正确匹配**。 + +### 消费语义的内部实现思路 + +`impl_run` 的实现逻辑很直观——检查状态、处理取消、调用可调用对象、更新状态。有几个细节值得提一下。 + +第一个是取消检查在执行前发生。`impl_run` 先检查令牌是否有效——如果已取消,直接消费回调但不执行,void 返回的情况直接 return,非 void 的情况抛出 `std::bad_function_call`。这个抛出异常的行为可能看起来有些激进,但它的理由很充分:调用方期望得到一个返回值,但我们无法提供一个有意义的值,所以抛异常是比返回未定义值更安全的做法。 + +第二个是 `if constexpr (std::is_void_v)` 的分支。当返回类型是 `void` 时,我们不能写 `ReturnType result = func_(args...)`——void 不是一种可以赋值的类型。`if constexpr` 在编译期选择分支,void 的情况走"调用但不赋值"的路径,非 void 的情况走"调用并赋值给 result"的路径。这是 `if constexpr` 处理 void 返回类型的标准模式。 + +第三个是消费后置空。`impl_run` 先把 `func_` move 出来作为局部变量,然后将 `func_` 置为 `nullptr`、`status_` 设为 kConsumed,最后执行局部变量里的可调用对象。这个顺序很重要——先把可调用对象拿出去、状态标记好,再执行。这样即使可调用对象内部抛出异常,`status_` 也已经是 kConsumed 了,回调不会处于一个不一致的状态。置空这一步不仅仅是标记状态——它触发了 `std::move_only_function` 析构其内部持有的可调用对象,释放 lambda 捕获的资源(比如 `unique_ptr`)。 + +### 验证核心骨架 + +骨架写完之后,快速验证几个场景就够了:基本类型返回、void 返回、move-only 捕获、移动语义。如果这四个场景都通过——构造回调能拿到正确的返回值、void 回调能正常执行、捕获 `unique_ptr` 的回调用完之后资源被释放、移动后源对象变空、目标对象有效——骨架就没有问题。完整的测试用例我们在第三篇统一整理。 + +--- + +## 第二步:参数绑定 — `bind_once()` + +### 我们要解决什么问题 + +`bind_once` 的场景很直观:你有一个三参数的函数 `f(int, int, int)`,但前两个参数在绑定时就能确定(比如 10 和 20),只有第三个参数要等到调用时才传入。你希望拿到一个只需传一个参数的 `OnceCallback`,调用时它自动把 10、20 和你传入的参数拼在一起喂给原函数。 + +这就是参数绑定——把"已知参数"提前塞进回调里,让调用方只需关心"未知参数"。Chromium 的 `BindOnce` 在这方面做了大量工作来处理参数的生命周期(`Unretained`、`Owned`、`Passed`、`WeakPtr` 等),我们的简化版只关注核心的参数绑定逻辑。 + +### `bind_once` 的实现骨架 + +```cpp +template +auto bind_once(F&& funtor, BoundArgs&&... args) { + return OnceCallback( + [f = std::forward(funtor), + ...bound = std::forward(args)] + (auto&&... call_args) mutable -> decltype(auto) { + return std::invoke( + std::move(f), + std::move(bound)..., + std::forward(call_args)... + ); + } + ); +} +``` + +这段代码不长,但里面有好几个值得展开讲的模板技巧。我们逐个拆。 + +### Lambda Capture Pack Expansion + +`...bound = std::forward(args)` 这一行是 C++20 引入的 **lambda 初始化捕获包展开**语法。它是整个 `bind_once` 能够简洁实现的关键。 + +在 C++20 之前,可变参数模板的参数包(parameter pack)不能直接展开到 lambda 的捕获列表里——你没法写 "把 `args...` 的每一个元素分别捕获到 lambda 里" 这样的代码。变通方案是用一个 `std::tuple` 把所有绑定参数打包存起来,然后在 lambda 内部用 `std::apply` 展开成单独的参数再调用。这个方案能用,但代码会膨胀很多——你需要一个额外的 tuple、一个 `std::apply` 调用、以及处理 tuple 元素移动语义的模板辅助代码。 + +C++20 终于允许了包展开进 lambda 捕获。具体来说,`...bound = std::forward(args)` 的效果是为 `BoundArgs...` 中的每一个类型生成一个对应的捕获变量,每个变量用 `std::forward` 完美转发初始化。举个具体例子,假设 `BoundArgs...` 是 `int, std::string`,那么展开后等价于: + +```cpp +[b1 = std::forward(arg1), b2 = std::forward(arg2)] +``` + +每个捕获变量在 lambda 内部都可以独立使用,而在我们的 `bind_once` 里,它们在 lambda 被调用时通过 `std::move(bound)...` 一起展开传给 `std::invoke`。注意这里用的是 `std::move` 而不是 `std::forward`——因为 lambda 是 `mutable` 的,捕获变量在 lambda 内部是左值,我们想把它们当作右值传出去以触发移动语义。 + +### `std::invoke` 的统一调用能力 + +lambda 内部用 `std::invoke` 而不是直接调用 `f(...)`,原因是 `std::invoke` 能统一处理各种可调用对象。普通函数指针直接调用没问题,但成员函数指针就不一样了——你没法写 `(&Class::method)(obj, args...)`,必须用 `(obj.*method)(args...)` 这种特殊语法。`std::invoke` 把这些差异全部封装了:`std::invoke(&Class::method, &obj, args...)` 等价于 `(obj.*method)(args...)`。 + +这意味着 `bind_once` 天然支持成员函数绑定,不需要额外的代码: + +```cpp +struct Calculator { + int multiply(int a, int b) { return a * b; } +}; + +Calculator calc; +auto bound = bind_once(&Calculator::multiply, &calc, 5); +int r = std::move(bound).run(8); // r == 40 +``` + +不过这里有一个**生命周期陷阱**需要注意:`&calc` 是裸指针,`bind_once` 不会管理它的生命周期。如果 `calc` 在回调被调用之前就被销毁了,`std::invoke` 会通过悬空指针访问已释放的内存。Chromium 用 `base::Unretained` 显式标记"我知道这个裸指针的生命周期是安全的",用 `base::Owned` 接管所有权,用 `base::WeakPtr` 在对象析构时自动取消回调。我们的简化版里,这个安全责任暂时交给调用方。 + +### 签名推导:为什么需要显式指定 `Signature` + +你可能注意到了 `bind_once` 的第一个模板参数 `Signature`(比如 `int(int)`)需要调用方显式指定。理想情况下,编译器应该能从 `F` 的可调用签名中自动推导出"去掉已绑定参数后的剩余签名"。但这件事在 C++ 里比想象中复杂得多。 + +对于函数指针 `R(*)(Args...)`,可以通过模板偏特化提取参数列表,然后用一种编译期的"类型列表切片"操作去掉前 N 个类型。对于有确定签名的仿函数(functor),也可以通过 `decltype(&T::operator())` 提取签名。但对于**泛型 lambda**(`[](auto x) { ... }`),它的 `operator()` 本身是模板,不存在唯一确定的签名——编译器根本无法在类型层面获取"这个 lambda 接受什么参数"的信息。 + +Chromium 为此写了一整套类型操作工具(`MakeUnboundRunType`、`DropTypeListItem` 等),大概有几百行模板元编程代码来处理各种边界情况。对于我们的教学目的,让调用方多写一个模板参数 `int(int)` 是更务实的选择——省去了大量复杂的模板元编程,代码清晰度也更好。 + +--- + +## 第三步:取消检查 — `is_cancelled()` 与 `maybe_valid()` + +### 取消令牌的概念 + +回调在创建时可以关联一个"取消令牌"(cancellation token)。令牌代表某个外部对象的生命周期——当那个对象被销毁后,令牌失效,通过令牌关联的所有回调都变为"已取消"状态。 + +你可以把它想象成一张"通行证":创建回调时发一张通行证给它,通行证上写着"有效"。某个时刻外部对象说"通行证作废了"(调用 `invalidate()`),之后所有持有这张通行证的回调在执行前检查时都会发现"通行证已经无效",跳过执行。在 Chromium 里,这个通行证就是 `WeakPtr` 内部的控制块——`WeakPtr` 指向的对象被销毁后,控制块中的标志位被清除,所有绑定到这个 `WeakPtr` 的回调自动取消。 + +### `CancelableToken` 的设计思路 + +我们的简化版取消令牌只需要三个核心操作:创建(生成有效令牌)、失效(标记为作废)、检查(查询是否还有效)。内部用 `shared_ptr` 管理一个包含 `atomic` 的 `Flag` 结构体: + +```cpp +class CancelableToken { + struct Flag { + std::atomic valid{true}; // 原子变量,多线程安全 + }; + // 所有 token 副本共享同一个 Flag + std::shared_ptr flag_; + +public: + CancelableToken() : flag_(std::make_shared()) {} + void invalidate() { flag_->valid.store(false, std::memory_order_release); } + bool is_valid() const { + return flag_->valid.load(std::memory_order_acquire); + } +}; +``` + +用 `shared_ptr` 而不是裸指针的原因是让令牌可以被拷贝和移动,同时所有副本共享同一个 `Flag`。`atomic` 保证多线程访问的安全性——一个线程可能在执行 `is_valid()` 的同时另一个线程在调 `invalidate()`,`memory_order_acquire/release` 语义保证前者的读一定能看到后者的写。 + +### 集成到 `OnceCallback` + +取消令牌集成到 `OnceCallback` 的方式很直接:在数据成员里加一个可选的 `shared_ptr`,通过 `set_token()` 方法设置,然后在两个地方检查它——`is_cancelled()` 查询时和 `impl_run()` 执行前。 + +`is_cancelled()` 的逻辑是:状态不是 kValid 就返回 true(空回调和已消费回调都算"已取消"),如果有令牌且令牌失效也返回 true。`impl_run` 里在真正执行可调用对象之前先检查令牌状态——如果已取消,消费回调但不执行,直接返回(void 情况)或者抛出 `std::bad_function_call`(需要返回值的情况)。 + +`maybe_valid()` 暂时就是 `!is_cancelled()` 的简单包装。在 Chromium 的完整实现中,两者的区别在于线程安全保证的强弱——`is_cancelled()` 只能在回调绑定的序列(即创建回调的线程)上调用,保证返回确定性结果;`maybe_valid()` 可以从任何线程调用,但结果可能过时。我们的简化版暂时不区分这个语义,但保留了两个方法名以备后续在 `RepeatingCallback` 或跨线程场景中扩展。 + +--- + +## 第四步:链式组合 — `then()` + +### `then()` 的语义 + +`then()` 允许我们把两个回调串联成一个管道。语义很直观:当管道被调用时,先用原始参数执行第一个回调,然后把返回值传给第二个回调。举个例子,回调 A 计算 `3 + 4 = 7`,回调 B 计算 `7 * 2 = 14`,用 `then()` 串联后,你得到一个新回调,调用它时自动走完 A → B 的整个流程。 + +听起来简单,但 `then()` 是四个功能里所有权设计最精巧的一个。 + +### 所有权是关键 + +串联后的新回调需要持有原回调和后续回调的**所有权**——否则原回调可能在外部被提前消费掉,管道就断了。而 `OnceCallback` 是 move-only 的,这意味着 `then()` 必须消费 `*this`(原回调)和 `next`(后续回调),把两者的所有权转移到一个新的 lambda 闭包里。整个所有权链条是这样的: + +```text +新回调 → move_only_function → lambda 闭包 → [原回调 + 后续回调] +``` + +实现思路的骨架大概是这样: + +```cpp +template +auto then(Next&& next) && // 末尾的 && 使其成为右值限定成员函数 + -> OnceCallback +{ + return OnceCallback( + [self = std::move(*this), // 把整个原回调移进 lambda + cont = std::forward(next)] // 把后续回调也移进来 + (FuncArgs... args) mutable -> decltype(auto) { + if constexpr (std::is_void_v) { + std::move(self).run(std::forward(args)...); + return std::invoke(std::move(cont)); // void → 无参数传递 + } else { + auto mid = std::move(self).run(std::forward(args)...); + return std::invoke(std::move(cont), std::move(mid)); // 传递中间结果 + } + } + ); +} +``` + +注意这里和 Chromium 原版的一个重要区别:我们对后续回调使用 `std::invoke` 而不是 `.run()`。这是因为 `then()` 接受的 `next` 参数是一个普通可调用对象(比如 lambda),不是 `OnceCallback`——调用方不需要显式地写 `std::move(cont).run()`,`std::invoke` 直接调用就好。只有 `self`(原回调)才需要 `std::move(...).run()` 来表达消费语义。 + +### 几个容易踩坑的地方 + +**第一,`&&` 限定。** 函数声明末尾的 `&&` 使其成为右值限定的成员函数,只能通过 `std::move(cb).then(next)` 或者临时对象 `.then(next)` 调用。这是另一种表达"消费语义"的方式——和 `run()` 用 deducing this 不同,`then()` 直接用传统的 ref-qualifier。为什么不用 deducing this?因为 `then()` 不需要区分左值和右值给出不同的错误信息——它就是只接受右值,没有中间地带。 + +**第二,`self = std::move(*this)`。** 这一行把当前 `OnceCallback` 对象的**所有内容**移动到 lambda 的闭包对象里。移动之后,当前对象进入已消费状态(因为我们没有把它设为 kEmpty,而是让它自然地保持一个"被移走"的状态)。闭包对象又被存入返回的新 `OnceCallback` 的 `move_only_function` 里——`move_only_function` 的类型擦除能力保证了不管 lambda 的实际类型是什么,都能被统一存储。 + +**第三,`mutable` 关键字不可省略。** Lambda 默认生成的 `operator()` 是 `const` 的——这意味着 lambda 内部不能修改捕获的变量。但我们需要在 lambda 内部对 `self` 调用 `std::move(self).run()`,这个操作会修改对象状态(把 status 从 kValid 改为 kConsumed)。所以 lambda 必须声明为 `mutable`,让 `operator()` 变成非 const 的。 + +**第四,`if constexpr (std::is_void_v)`。** 和 `impl_run` 里的情况一样——当原回调返回 `void` 时,`then()` 的语义是"先执行原回调,再执行后续回调(无参数传递)"。`if constexpr` 在编译期选择分支,两种情况生成完全不同的代码路径。 + +### 多级管道 + +`then()` 可以链式调用,形成多级管道: + +```cpp +using namespace tamcpp::chrome; +auto pipeline = OnceCallback([](int x) { + return x * 2; +}).then([](int x) { + return x + 10; +}).then([](int x) { + return std::to_string(x); +}); + +std::string result = std::move(pipeline).run(5); +// 5 * 2 = 10, 10 + 10 = 20, "20" +``` + +每次 `then()` 调用都会创建一个新的 `once_callback`,内部嵌套捕获了前一步的回调。从外到内的调用顺序是递归展开的:最外层回调被 `run()` → 执行其 lambda → lambda 内部对上一层调用 `std::move(self).run()` → 再对更上一层调用 → 直到底层。性能上,每一层 `then()` 增加一次 `std::move_only_function` 的间接调用,对于 2-3 级管道来说完全可接受。如果管道层级很深(超过 10 级),可以考虑用 `std::variant` 做一个扁平化的管道结构来避免嵌套闭包的开销——但这已经超出我们当前的讨论范围了。 + +--- + +## 小结 + +这一篇我们完成了 `OnceCallback` 四个核心功能的设计走读。和第一篇的接口设计不同,这里的重点是理解"为什么这样写"和"关键的模板技巧是什么"。几个核心知识点回顾一下: + +- **模板偏特化** `OnceCallback` 让用户可以用自然的函数签名语法来指定回调类型,编译器通过模式匹配把函数类型拆解成返回值和参数包 +- **Deducing this** 让 `run()` 通过一个函数模板实现编译期的左值/右值拦截,比 Chromium 的双重重载 + `!sizeof` hack 更干净 +- **`requires` 约束**(通过 `not_the_same_t` concept)解决了模板构造函数与移动构造函数的匹配冲突,是 move-only 类型擦除包装器的标准防御手段 +- **Lambda capture pack expansion** 是 `bind_once` 得以简洁实现的关键,C++20 之前需要用 tuple + apply 的变通方案 +- **`then()` 的核心挑战**是所有权管理——它通过右值限定 + lambda 捕获 move 来保证管道中每个回调的所有权链完整,对后续回调使用 `std::invoke` 统一调用 + +下一篇我们会用系统化的测试用例来验证这些设计,并对比我们与 Chromium 原版在性能上的取舍。 + +## 参考资源 + +- [Chromium callback.h 源码](https://chromium.googlesource.com/chromium/src/+/HEAD/base/functional/callback.h) +- [Chromium bind_internal.h 源码](https://chromium.googlesource.com/chromium/src/+/HEAD/base/functional/bind_internal.h) +- [cppreference: std::move_only_function](https://en.cppreference.com/w/cpp/utility/functional/move_only_function) +- [cppreference: std::invoke](https://en.cppreference.com/w/cpp/utility/functional/invoke) +- [P0847R7 - Deducing this 提案](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0847r7.html) +- [P0780R2 - Pack Expansion in Lambda Capture](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0780r2.html) diff --git a/documents/vol9-open-source-project-learn/chrome/01_once_callback/hands_on/03-once-callback-testing.md b/documents/vol9-open-source-project-learn/chrome/01_once_callback/hands_on/03-once-callback-testing.md new file mode 100644 index 000000000..d59c1b822 --- /dev/null +++ b/documents/vol9-open-source-project-learn/chrome/01_once_callback/hands_on/03-once-callback-testing.md @@ -0,0 +1,296 @@ +--- +title: "once_callback 设计指南(三):测试策略与性能对比" +description: "系统设计 once_callback 的测试用例,对比与 Chromium 原版和标准库方案的性能差异,总结设计取舍" +chapter: 1 +order: 3 +tags: + - host + - cpp-modern + - advanced + - 回调机制 + - 函数对象 +difficulty: advanced +platform: host +cpp_standard: [23] +reading_time_minutes: 20 +prerequisites: + - "once_callback 设计指南(一):动机与接口设计" + - "once_callback 设计指南(二):逐步实现" +related: + - "回调取消与组合模式" +--- + +# once_callback 设计指南(三):测试策略与性能对比 + +## 引言 + +前两篇我们完成了 `OnceCallback` 的设计和实现。这一篇做两件事:第一,系统化地梳理测试策略,给出一套完整的测试用例清单,确保我们的实现在各种边界条件下都是正确的;第二,从性能角度分析我们的实现与 Chromium 原版、标准库方案之间的差异,弄清楚我们牺牲了什么、换来了什么。 + +> **学习目标** +> +> - 掌握 `OnceCallback` 的六类测试用例设计方法 +> - 理解 `sizeof`、SBO 阈值、间接调用开销等性能指标的含义 +> - 清楚我们的 `OnceCallback` 与 Chromium `OnceCallback` 的取舍关系 + +--- + +## 测试策略 + +我们把测试组织成六个类别,每个类别聚焦一个独立的设计不变量。这种按不变量组织测试的方式比按功能组织更不容易遗漏边界情况——因为每个不变量本身就是一种正确性保证,测试的目的就是验证这些保证在各种场景下都成立。 + +我们的实际测试代码使用 Catch2 框架,配合 CMake + CPM 管理依赖。下面列出的测试用例与 `code/volumn_codes/vol9/chrome_design/test/test_once_callback.cpp` 中的实际代码一一对应。 + +### A 类:基本调用与返回值 + +这类测试验证 `OnceCallback` 的基本构造和调用行为。 + +```cpp +TEST_CASE("non-void return", "[once_callback]") { + OnceCallback cb([](int a, int b) { return a + b; }); + int result = std::move(cb).run(3, 4); + REQUIRE(result == 7); +} + +TEST_CASE("void return", "[once_callback]") { + bool called = false; + OnceCallback cb([&called] { called = true; }); + std::move(cb).run(); + REQUIRE(called); +} +``` + +最基本的场景——构造一个回调,调用它,验证返回值。void 返回类型走的是 `if constexpr (std::is_void_v)` 的另一条分支,确认我们的编译期分支逻辑是正确的。 + +### B 类:移动语义 + +这类测试验证 move-only 约束和移动操作的正确性。 + +```cpp +TEST_CASE("move-only capture", "[once_callback]") { + auto ptr = std::make_unique(42); + OnceCallback cb([p = std::move(ptr)] { return *p; }); + int result = std::move(cb).run(); + REQUIRE(result == 42); +} + +TEST_CASE("move semantics: source becomes null", "[once_callback]") { + OnceCallback cb([] { return 1; }); + OnceCallback cb2 = std::move(cb); + REQUIRE(cb.is_null()); + + int result = std::move(cb2).run(); + REQUIRE(result == 1); +} +``` + +move-only capture 测试(`std::make_unique(42)` 被捕获进 lambda)确认了 `OnceCallback` 真正支持 move-only 的可调用对象——如果底层用的是 `std::function` 而不是 `std::move_only_function`,这段代码直接编译失败。移动语义测试验证了移动构造后源对象变为 `kEmpty` 状态(通过 `is_null()` 检查),目标对象保持有效并可以正常调用。 + +有一个容易搞混的概念点——移动操作转移了所有权,但不会触发消费。只有 `run()` 才会消费回调。这个区别在 Chromium 里也很重要:`PostTask(FROM_HERE, std::move(cb))` 只是转移所有权,回调在任务被执行之前一直处于活跃状态。 + +### C 类:单次调用约束 + +这类测试验证"调用一次即消费"的核心语义。在 A 类和 B 类的测试中我们已经覆盖了正常调用路径,C 类聚焦于左值调用的编译拦截。这个约束是通过 deducing this + `static_assert` 实现的——如果写 `cb.run()` 而不是 `std::move(cb).run()`,编译器会直接报错,错误信息明确告诉调用方应该用 `std::move`。这部分不需要运行时测试,编译通过本身就是验证。 + +### D 类:参数绑定 + +```cpp +TEST_CASE("bind_once basic", "[bind_once]") { + auto bound = bind_once([](int a, int b) { return a * b; }, 5); + int result = std::move(bound).run(8); + REQUIRE(result == 40); +} + +TEST_CASE("bind_once with member function", "[bind_once]") { + struct Calc { + int multiply(int a, int b) { return a * b; } + }; + Calc calc; + auto bound = bind_once(&Calc::multiply, &calc, 5); + int result = std::move(bound).run(8); + REQUIRE(result == 40); +} +``` + +`bind_once` 测试覆盖了两种典型场景:普通 lambda 的部分参数绑定和成员函数绑定。成员函数绑定测试特别值得关注——`&Calc::multiply` 是成员函数指针,`&calc` 是对象指针,`std::invoke` 在内部把它展开成 `(calc.*multiply)(5, 8)` 调用。这里有一个生命周期陷阱需要注意:`&calc` 是裸指针,`bind_once` 不会管理它的生命周期。如果 `calc` 在回调被调用之前就被销毁了,`std::invoke` 会通过悬空指针访问已释放的内存。Chromium 用 `base::Unretained` 显式标记裸指针的安全性,用 `base::Owned` 接管所有权,用 `base::WeakPtr` 在对象析构时自动取消回调。我们的简化版里,这个安全责任暂时交给调用方。 + +### E 类:取消机制 + +```cpp +TEST_CASE("is_cancelled respects cancel token", "[once_callback]") { + auto token = std::make_shared(); + OnceCallback cb([] {}); + cb.set_token(token); + + REQUIRE_FALSE(cb.is_cancelled()); + token->invalidate(); + REQUIRE(cb.is_cancelled()); +} + +TEST_CASE("cancelled void callback does not execute", "[once_callback]") { + auto token = std::make_shared(); + bool called = false; + OnceCallback cb([&called] { called = true; }); + cb.set_token(token); + token->invalidate(); + + std::move(cb).run(); + REQUIRE_FALSE(called); +} + +TEST_CASE("cancelled non-void callback throws", "[once_callback]") { + auto token = std::make_shared(); + OnceCallback cb([] { return 1; }); + cb.set_token(token); + token->invalidate(); + + REQUIRE_THROWS_AS(std::move(cb).run(), std::bad_function_call); +} +``` + +取消测试覆盖了三个关键行为:令牌有效时不取消、令牌失效后 void 回调不执行、令牌失效后非 void 回调抛出 `std::bad_function_call`。第三个测试的行为值得展开说一下——我们的实现在非 void 返回的已取消回调中抛出异常,理由是调用方期望得到一个返回值,但我们无法提供一个有意义的值,所以抛异常是比返回未定义值更安全的做法。Chromium 的实现在这里会直接终止程序(`CHECK` 失败),我们选择异常是因为它在测试中更容易捕获和验证。 + +### F 类:Then 组合 + +```cpp +TEST_CASE("then chains two callbacks", "[then]") { + auto cb = OnceCallback([](int x) { return x * 2; }) + .then([](int x) { return x + 10; }); + int result = std::move(cb).run(5); + REQUIRE(result == 20); // 5 * 2 + 10 +} + +TEST_CASE("then multi-level pipeline", "[then]") { + auto pipeline = OnceCallback([](int x) { return x * 2; }) + .then([](int x) { return x + 10; }) + .then([](int x) { return std::to_string(x); }); + std::string result = std::move(pipeline).run(5); + REQUIRE(result == "20"); // (5*2)+10 = "20" +} + +TEST_CASE("then with void first callback", "[then]") { + int value = 0; + auto cb = OnceCallback([&value](int x) { value = x; }) + .then([&value] { return value * 3; }); + int result = std::move(cb).run(7); + REQUIRE(result == 21); +} +``` + +`then()` 测试覆盖了三种组合模式:两级非 void 管道、多级管道(跨越类型边界——从 `int` 到 `std::string`)、以及 void 前缀回调。多级管道测试特别有趣——`(5*2)+10 = 20`,最终被 `std::to_string` 转换为字符串 `"20"`。这个测试验证了 `then()` 在每一级都正确地推导了返回类型,并且类型擦除(通过 `std::move_only_function`)在不同类型的 lambda 之间正确工作。void 前缀测试验证了 `if constexpr (std::is_void_v)` 分支——第一个回调设置 `value = 7`,第二个回调通过引用读取 `value` 并返回 `21`。 + +### 测试框架与构建配置 + +我们使用 Catch2 v3 作为测试框架,通过 CPM(CMake Package Manager)自动拉取依赖。测试的 CMake 配置非常简洁: + +```cmake +# test/CMakeLists.txt +CPMAddPackage("gh:catchorg/Catch2@3.7.1") + +add_executable(test_once_callback test_once_callback.cpp) +target_link_libraries(test_once_callback PRIVATE once_callback Catch2::Catch2WithMain) +target_compile_options(test_once_callback PRIVATE -Wall -Wextra -Wpedantic) + +add_test(NAME test_once_callback COMMAND test_once_callback) +``` + +Catch2 的 `REQUIRE` 宏比 `assert()` 强在它会报告具体的失败表达式、文件和行号,并且在同一个 `TEST_CASE` 内继续执行后续检查(而不是像 `assert()` 那样直接终止程序)。`REQUIRE_THROWS_AS` 则专门用于验证异常类型——在取消机制的测试中,我们需要确认被取消的非 void 回调抛出的是 `std::bad_function_call`,而不是其他异常。 + +运行测试的流程很简单——在 `build/` 目录下 `cmake --build . && ctest`。 + +--- + +## 性能考量:与 Chromium 原版对比 + +### 对象大小 + +这是最直观的差异。我们用一个简单的程序来测量: + +```cpp +#include +#include +#include "once_callback/once_callback.hpp" + +int main() { + std::cout << "sizeof(std::function): " + << sizeof(std::function) << " bytes\n"; + std::cout << "sizeof(std::move_only_function): " + << sizeof(std::move_only_function) << " bytes\n"; + // Chromium OnceCallback ≈ 8 bytes(一个指针) + + using namespace tamcpp::chrome; + std::cout << "sizeof(OnceCallback): " + << sizeof(OnceCallback) << " bytes\n"; + // 我们的 OnceCallback 大约是: + // move_only_function (32) + status (1) + token ptr (16) + padding + // 预估 56-64 bytes +} +``` + +在 GCC 上,典型值如下:`std::function` 约 32 字节,`std::move_only_function` 约 32 字节,我们的 `OnceCallback` 加上 `Status` 枚举和可选的 `CancelableToken` 指针,大约 56-64 字节。Chromium 的 `OnceCallback` 只有 8 字节——一个指向 `BindState` 的 `scoped_refptr`。 + +差距的根源在于存储策略。Chromium 把所有状态(可调用对象 + 绑定参数)都放在堆上的 `BindState` 里,回调对象本身只持有一个指针。我们用 `std::move_only_function` 的 SBO 把小对象直接内联存储在回调对象内部,避免了堆分配但增大了对象大小。 + +### 分配行为 + +`std::move_only_function` 的 SBO 阈值是实现定义的,通常是 2-3 个指针大小(16-24 字节)。捕获少量参数的 lambda(比如 `[x = 42]` 或 `[&ref]`)通常能放进 SBO,不会触发堆分配。但如果 lambda 捕获了大量数据(比如一个 `std::string` + 几个 `int`),就会在构造时堆分配。 + +Chromium 的方案总是堆分配(`new BindState`),但分配只发生一次——在 `BindOnce` 时。之后 `OnceCallback` 的移动操作只是复制一个指针(8 字节),代价极低。我们的方案在小对象时不分配(SBO),但移动操作需要复制整个 `std::move_only_function`(32 字节)加上 `token_` 指针,代价稍高。 + +两种策略在不同场景下各有优势。对于高频投递的小回调(Chrome 浏览器的主场景),Chromium 的方案更优——移动代价低、大小一致有利于 CPU 缓存。对于低频的大回调(比如一次性初始化任务),我们的方案更优——省去一次堆分配。 + +### 间接调用开销 + +两种方案的调用开销是一样的:一次间接函数调用。`std::move_only_function::operator()` 内部通过函数指针或虚函数表分派到具体的可调用对象;Chromium 的 `BindState::polymorphic_invoke_` 也是函数指针分派。在 `-O2` 优化下,这个间接调用无法被内联消除,性能上两种方案等价。 + +### 我们牺牲了什么,换来了什么 + +总结一下取舍。 + +我们牺牲了对象的紧凑性(56-64 字节 vs 8 字节),换来了实现简洁性——不需要手写引用计数、函数指针表、`TRIVIAL_ABI` 注解。我们牺牲了移动操作的极致性能(复制 32 字节 + 指针 vs 复制 8 字节),换来了小对象的零堆分配。我们牺牲了引用计数共享(无法让多个回调共享同一份 `BindState`),但 `OnceCallback` 本身就是独占语义,不需要共享。 + +这些取舍对于教学目的和大多数实际场景来说都是合理的。如果你的项目确实需要 Chromium 级别的极致性能,可以参考 Chromium 的源码做进一步优化——核心思路我们已经在这三篇设计指南里讲清楚了。 + +--- + +## 完整组件文件一览 + +到这里,`OnceCallback` 组件的设计、实现和测试策略都已完成。完整的文件清单: + +```text +documents/vol9-open-source-project-learn/chrome/hands_on/ +├── 01-once-callback-design.md # 设计篇:动机与接口 +├── 02-once-callback-implementation.md # 实现篇:逐步实现 +└── 03-once-callback-testing.md # 验证篇:测试与性能 +``` + +对应的可编译代码(头文件 + 测试)位于项目代码目录中: + +```text +code/volumn_codes/vol9/chrome_design/ +├── CMakeLists.txt +├── cmake/CPM.cmake +├── cancel_token/ +│ └── cancel_token.hpp # 取消令牌 +├── once_callback/ +│ ├── CMakeLists.txt +│ ├── once_callback.hpp # 主接口(模板声明) +│ └── once_callback_impl.hpp # 实现(模板定义) +└── test/ + ├── CMakeLists.txt # Catch2 测试配置 + └── test_once_callback.cpp # 完整测试用例 +``` + +--- + +## 小结 + +这篇验证篇我们做了两件事。测试方面,围绕六个不变量(基本调用、移动语义、单次调用、参数绑定、取消机制、链式组合)设计了 11 个 Catch2 测试用例,覆盖了 `OnceCallback` 的所有核心行为。性能方面,对比了与 Chromium `OnceCallback` 在对象大小、分配行为和调用开销上的差异——我们的实现用紧凑性换来了简洁性,对绝大多数场景来说这个取舍是值得的。 + +下一步可以尝试的方向:实现 `RepeatingCallback`(可复制、可重复调用的版本),给 `bind_once` 添加 `Unretained` / `Owned` / `WeakPtr` 等生命周期辅助函数,或者用 Google Benchmark 做精确的性能测量。 + +## 参考资源 + +- [Chromium base/functional/ 源码目录](https://source.chromium.org/chromium/chromium/src/+/main:base/functional/) +- [cppreference: std::move_only_function](https://en.cppreference.com/w/cpp/utility/functional/move_only_function) +- [Google Test 文档](https://google.github.io/googletest/) +- [Google Benchmark 文档](https://github.com/google/benchmark) diff --git a/documents/vol9-open-source-project-learn/chrome/01_once_callback/hands_on/index.md b/documents/vol9-open-source-project-learn/chrome/01_once_callback/hands_on/index.md new file mode 100644 index 000000000..b6d4ae482 --- /dev/null +++ b/documents/vol9-open-source-project-learn/chrome/01_once_callback/hands_on/index.md @@ -0,0 +1,7 @@ +# 进阶设计指南 + +本目录包含 OnceCallback 组件的三篇设计指南,面向有 C++ 模板经验的读者。如果你对移动语义、模板偏特化、类型擦除等概念还不熟悉,建议先阅读 [full/](../full/) 目录下的前置知识文章。 + +- [设计指南(一):动机与接口设计](01-once-callback-design.md) +- [设计指南(二):逐步实现](02-once-callback-implementation.md) +- [设计指南(三):测试策略与性能对比](03-once-callback-testing.md) diff --git a/documents/vol9-open-source-project-learn/chrome/01_once_callback/index.md b/documents/vol9-open-source-project-learn/chrome/01_once_callback/index.md new file mode 100644 index 000000000..62a5e15d5 --- /dev/null +++ b/documents/vol9-open-source-project-learn/chrome/01_once_callback/index.md @@ -0,0 +1,34 @@ +# OnceCallback:从 Chromium 学到的回调设计 + +本目录通过实现 Chromium 风格的 `OnceCallback` 组件,系统讲解现代 C++ 回调系统的设计。内容分为两个学习路径: + +## 新手完整教程(full/) + +面向零基础读者,从 C++ 基础特性复习开始,逐步引导到完整的组件实现。 + +**前置知识(7 篇):** + +- [OnceCallback 前置知识速查:C++11/14/17 核心特性回顾](full/pre-00-once-callback-cpp-basics-review.md) +- [OnceCallback 前置知识(一):函数类型与模板偏特化](full/pre-01-once-callback-function-type-and-specialization.md) +- [OnceCallback 前置知识(二):std::invoke 与统一调用协议](full/pre-02-once-callback-invoke-and-callable.md) +- [OnceCallback 前置知识(三):Lambda 高级特性](full/pre-03-once-callback-lambda-advanced.md) +- [OnceCallback 前置知识(四):Concepts 与 requires 约束](full/pre-04-once-callback-concepts-and-requires.md) +- [OnceCallback 前置知识(五):std::move_only_function (C++23)](full/pre-05-once-callback-move-only-function.md) +- [OnceCallback 前置知识(六):Deducing this (C++23)](full/pre-06-once-callback-deducing-this.md) + +**动手实践(6 篇):** + +- [OnceCallback 实战(一):动机与接口设计](full/01-1-once-callback-motivation-and-api-design.md) +- [OnceCallback 实战(二):核心骨架搭建](full/01-2-once-callback-core-skeleton.md) +- [OnceCallback 实战(三):bind_once 实现](full/01-3-once-callback-bind-once.md) +- [OnceCallback 实战(四):取消令牌设计](full/01-4-once-callback-cancellation-token.md) +- [OnceCallback 实战(五):then 链式组合](full/01-5-once-callback-then-chaining.md) +- [OnceCallback 实战(六):测试与性能对比](full/01-6-once-callback-testing-and-perf.md) + +## 进阶设计指南(hands_on/) + +面向有 C++ 模板经验的读者,快速走读设计动机、实现策略和测试验证。 + +- [once_callback 设计指南(一):动机与接口设计](hands_on/01-once-callback-design.md) +- [once_callback 设计指南(二):逐步实现](hands_on/02-once-callback-implementation.md) +- [once_callback 设计指南(三):测试策略与性能对比](hands_on/03-once-callback-testing.md) diff --git a/scripts/build_examples.py b/scripts/build_examples.py index 41255286b..c44162692 100644 --- a/scripts/build_examples.py +++ b/scripts/build_examples.py @@ -105,7 +105,8 @@ def build_project(project_dir: Path) -> BuildResult: all_output = [] # Configure - configure_cmd = ['cmake', '-B', str(build_dir), '-G', 'Ninja'] + configure_cmd = ['cmake', '-B', str(build_dir), '-G', 'Ninja', + '-DCMAKE_CXX_COMPILER_LAUNCHER=ccache'] try: result = subprocess.run( configure_cmd, diff --git a/todo/content/210-vol9-chrome-base-outline.md b/todo/content/210-vol9-chrome-base-outline.md new file mode 100644 index 000000000..a2c2b83c3 --- /dev/null +++ b/todo/content/210-vol9-chrome-base-outline.md @@ -0,0 +1,225 @@ +--- +id: "210" +title: "卷九·开源项目学习 — Chrome Base Library 设计模式大纲" +category: content +priority: P2 +status: pending +created: 2026-05-01 +assignee: charliechen +depends_on: ["200", "201"] +blocks: [] +estimated_effort: large +--- + +# 卷九·开源项目学习 — Chrome Base Library 设计模式大纲 + +## 总览 + +- **卷名**:vol9-open-source-project-learn +- **章节名**:chrome/(vol9-1) +- **难度范围**:intermediate → advanced +- **预计文章数**:22 篇 +- **前置知识**:卷一 + 卷二(RAII、移动语义、智能指针、lambda),卷五(并发基础) +- **C++ 标准覆盖**:C++11-23 +- **目录位置**:`documents/vol9-open-source-project-learn/chrome/` +- **学习目标**:通过剖析 Chromium `base` 库中与浏览器无关的通用组件,掌握大型 C++ 项目的基础库设计范式,理解工业级代码如何平衡类型安全、性能与可测试性 + +## 章节大纲 + +### ch00:导论 — 为什么读 Chrome 源码 + +- **预计篇数**:2 + +| 编号 | 文件名 | 标题 | 核心内容 | C++ 标准关联 | +|------|--------|------|---------|-------------| +| 00-01 | 01-why-chrome-base.md | 为什么选择 Chrome Base Library | Chrome base 库的设计哲学:响应式优先、消息传递优于锁、序列化优于线程、编译期类型安全、性能优先基准测试驱动、测试优先架构;base 库与浏览器组件的边界;Chrome base 与 Abseil、Folly 的定位对比 | C++11-23 | +| 00-02 | 02-chrome-build-structure.md | Chrome Base 源码结构与构建系统 | 目录组织(`base/functional/`、`base/memory/`、`base/containers/`、`base/task/`、`base/strings/`、`base/time/`、`base/synchronization/`、`base/threading/`);GN 构建系统概览;如何在本地拉取并编译 Chrome base 单元测试;独立提取 base 组件的方法 | C++17 | + +### ch01:回调系统 — 类型安全的函数抽象 + +- **预计篇数**:3 + +| 编号 | 文件名 | 标题 | 核心内容 | C++ 标准关联 | +|------|--------|------|---------|-------------| +| 01-01 | 03-once-repeating-callback.md | OnceCallback 与 RepeatingCallback | `base::OnceCallback` 与 `base::RepeatingCallback` 的类型设计;Once 的移动语义保证(仅可调用一次);Repeating 的复制语义;`TRIVIAL_ABI` 注解对移动行为的影响;与 `std::function` 的对比(移动语义保证、WeakPtr 集成、Unretained 参数保护) | C++11 `std::function`,C++23 `std::move_only_function` | +| 01-02 | 04-bind-and-argument-binding.md | base::BindOnce / BindRepeating 与参数绑定 | `base::BindOnce` / `base::BindRepeating` 的参数绑定机制;`base::Unretained`(裸指针,不做生命周期管理)、`base::Owned`(接管所有权)、`base::Passed`(移动语义传递)、`base::WeakPtr`(弱引用自动取消)等绑定辅助函数的设计意图与安全模型;与 `std::bind` 和 lambda capture 的对比 | C++11 `std::bind`,C++14 泛型 lambda | +| 01-03 | 05-callback-cancellation-composition.md | 回调取消与组合模式 | `IsCancelled()` / `MaybeValid()` 双层检查机制;`Then()` 链式组合(monadic 风格);`base::CallbackList`(一对多回调分发);`base::CancelableCallback`(取消注册);`base::BarrierCallback` / `base::BarrierClosure`(等待 N 个回调齐备后触发);实战:设计一个异步任务管道 | C++23 `std::move_only_function` 的组合模式 | + +### ch02:内存管理 — 超越 shared_ptr 的安全模型 + +- **预计篇数**:3 + +| 编号 | 文件名 | 标题 | 核心内容 | C++ 标准关联 | +|------|--------|------|---------|-------------| +| 02-01 | 06-weak-ptr-factory.md | base::WeakPtr 与 WeakPtrFactory | `WeakPtrFactory` 放置位置的最佳实践(作为最后一个成员变量以确保先于其他成员失效);`WeakReference::Flag` 内部的 `AtomicFlag` + `RefCountedThreadSafe` 实现原理;序列绑定(`SEQUENCE_CHECKER`)的线程安全模型;`MaybeValid()` 的跨线程优化用途;与 `std::weak_ptr` 的性能与语义对比(WeakPtr 不需要 `shared_ptr` 控制块) | C++11 `std::weak_ptr`,C++20 `requires` 约束 | +| 02-02 | 07-scoped-refptr-intrusive.md | scoped_refptr 与侵入式引用计数 | `base::RefCounted` / `base::RefCountedThreadSafe` 的侵入式引用计数设计;`scoped_refptr` 的 RAII 包装;与 `std::shared_ptr` 的对比:侵入式 vs 非侵入式的性能差异(减少一次堆分配、更快的引用计数操作);`AddRef` / `Release` 的线程安全实现;何时选择侵入式引用计数 | C++11 `std::shared_ptr`,`std::make_shared` | +| 02-03 | 08-raw-ptr-miracleptr.md | raw_ptr — use-after-free 防护 | `base::raw_ptr`("MiraclePtr")的设计动机与实现:通过内存投毒(memory poisoning)检测 use-after-free;`RAW_PTR_EXCLUSION` 排除标记;`raw_ptr` vs 裸指针 vs `unique_ptr` 的性能基准测试数据;`PartitionAlloc` 分配器协作机制;这个设计对日常 C++ 开发的启发 | C++11 `std::unique_ptr` | + +### ch03:线程与任务系统 — 序列优于线程 + +- **预计篇数**:3 + +| 编号 | 文件名 | 标题 | 核心内容 | C++ 标准关联 | +|------|--------|------|---------|-------------| +| 03-01 | 09-task-runner-hierarchy.md | TaskRunner 层级体系 | `base::TaskRunner` → `base::SequencedTaskRunner` → `base::SingleThreadTaskRunner` 的继承层次设计;`base::TaskTraits`(优先级、线程池选择、是否阻塞等属性);`base::PostTask()` / `base::PostTaskAndReply()` 的使用模式;与 `std::async` 的设计哲学对比 | C++11 `std::async`,C++20 `std::jthread` | +| 03-02 | 10-sequences-over-threads.md | 序列优于线程哲学 | Chrome 的核心理念:Sequence 是逻辑上的虚拟线程,保证任务顺序执行但不绑定物理线程;`base::SequenceChecker` / `base::SequenceToken` 的实现;`base::SequenceBound` 跨序列对象管理;为什么消息传递优于锁:`base::PostTask` 替代 mutex 的设计范式 | C++11 `std::mutex`,C++20 `std::jthread` | +| 03-03 | 11-thread-pool-internals.md | 线程池与任务调度内幕 | `base::ThreadPoolImpl` 的架构:`TaskTracker`(生命周期追踪)、`Sequence`(任务序列)、`WorkerThread`(工作线程)、优先级队列(`PriorityQueue`)、延迟任务管理(`DelayedTaskManager`);任务窃取策略;`base::Job` 并行任务源;性能考量:任务粒度、优先级反转防护 | C++17 并行算法,C++26 Senders 提案 | + +### ch04:自定义容器 — 面向缓存友好的数据结构 + +- **预计篇数**:3 + +| 编号 | 文件名 | 标题 | 核心内容 | C++ 标准关联 | +|------|--------|------|---------|-------------| +| 04-01 | 12-flat-containers.md | flat_map / flat_set — 排序向量容器 | `base::flat_map` / `base::flat_set` 基于排序 `std::vector` 的实现;`base::flat_tree` 内部实现(二分查找 + 插入/删除的复杂度分析);缓存友好性分析:连续内存 vs 红黑树节点;适用场景(小数据集、读多写少);`base::fixed_flat_map` / `fixed_flat_set`(编译期固定大小);与 `std::map` / `std::set` 的性能对比 | C++23 `std::flat_map` / `std::flat_set` | +| 04-02 | 13-small-map-circular-deque.md | small_map 与 circular_deque | `base::small_map` 的小缓冲区优化(SBO):小数据量用 `std::vector` 线性搜索,大数据量自动切换为 `std::map` 红黑树;切换阈值的性能调优;`base::circular_deque` 的分段连续存储实现(避免 `std::deque` 的跳表开销);环形缓冲区在音视频/网络场景的应用 | C++11 `std::deque`,LLVM `SmallVector` | +| 04-03 | 14-span-and-buffer-safety.md | base::span 与安全缓冲区访问 | `base::span` 的设计(先于 C++20 `std::span` 的实现);`base::BufferIterator`(类型安全的二进制解析);`base::SpanReader` / `base::SpanWriter`(读写视图);`base::checked_iterators` 的边界检查;Chrome 从 `base::span` 迁移到 `std::span` 的经验教训 | C++20 `std::span`,C++26 `std::spanstream` | + +### ch05:同步原语与 RAII 守卫 + +- **预计篇数**:3 + +| 编号 | 文件名 | 标题 | 核心内容 | C++ 标准关联 | +|------|--------|------|---------|-------------| +| 05-01 | 15-raii-guards-auto-lock.md | RAII 守卫体系 | `base::AutoLock` / `base::AutoUnlock` / `base::AutoReset` 的 RAII 设计;`base::ScopedGeneric` 通用 RAII 包装器模板;`base::ScopedFILE` / `base::ScopedFD` 文件描述符管理;`base::ScopedNativeLibrary` 动态库句柄管理;设计你自己的 RAII 守卫的实践模式 | C++11 `std::lock_guard`,`std::unique_lock` | +| 05-02 | 16-waitable-event-atomic-flag.md | WaitableEvent 与 AtomicFlag | `base::WaitableEvent` 的跨平台实现(Linux `eventfd` / Windows `Event` / macOS `pthread_cond`);`base::AtomicFlag` 的一次性同步标志(比 `std::atomic` 更轻量);`base::ConditionVariable` 的设计;`base::CancelableEvent` 的取消语义 | C++11 `std::condition_variable`,`std::atomic` | +| 05-03 | 17-thread-restrictions.md | 线程限制与静态分析 | `base::ThreadRestrictions` — 在编译期/运行时禁止某些操作(如禁止在 IO 线程执行文件操作);`base::ScopedBlockingCall` — 声明阻塞调用以帮助线程池调度;`base::thread_annotations.h` 中的 Clang 静态分析注解(`GUARDED_BY`、`REQUIRES`、`ACQUIRED_BEFORE`);这个设计如何应用到日常项目中防止线程错误 | C++11 `std::mutex`,C++20 contracts 提案 | + +### ch06:字符串、时间与日志 — 基础工具的设计艺术 + +- **预计篇数**:3 + +| 编号 | 文件名 | 标题 | 核心内容 | C++ 标准关联 | +|------|--------|------|---------|-------------| +| 06-01 | 18-string-piece-string-utils.md | StringPiece 与字符串工具 | `base::StringPiece`(先于 C++17 `std::string_view` 的实现);Chrome 迁移到 `std::string_view` 的经验;`base::SplitString` / `base::JoinString` / `base::StringTokenizer` 的设计;`base::StringPrintf`(类型安全的格式化);`base::NumberToString` / `base::StringToNumber` 的错误处理;UTF-8/UTF-16 转换工具 | C++17 `std::string_view`,C++23 `std::print` | +| 06-02 | 19-time-typed-arithmetic.md | Time / TimeDelta — 类型安全的时间运算 | `base::Time` / `base::TimeDelta` / `base::TimeTicks` 的设计:用强类型防止时间单位混淆(`TimeDelta::FromMilliseconds(100)` vs 裸整数);`base::Clock` / `base::TickClock` 的抽象接口(可注入用于测试);`base::TimeDeltaFromString` 的解析;与 `std::chrono` 的对比:API 易用性 vs 标准化 | C++11 `std::chrono`,C++20 `std::chrono::calendar` | +| 06-03 | 20-logging-check-macros.md | LOG / CHECK / DCHECK 宏设计 | `LOG(INFO/WARNING/ERROR/FATAL)` 流式日志设计;`CHECK()`(release+debug)vs `DCHECK()`(debug-only)的分层断言;`DUMP_WILL_BE_CHECK()` 的渐进式上线策略(`NotFatalUntil` 里程碑参数);`NOTREACHED()` / `NOTIMPLEMENTED()` 的语义区分;`base::Location` 源码位置追踪;`VLOG(n)` 模块化详细日志;如何在自己的项目中设计类似的日志/断言系统 | C++11 `assert`,C++23 `std::stacktrace`,C++26 契约编程提案 | + +### ch07:设计模式与架构思想 + +- **预计篇数**:2 + +| 编号 | 文件名 | 标题 | 核心内容 | C++ 标准关联 | +|------|--------|------|---------|-------------| +| 07-01 | 21-observer-delegate-patterns.md | Observer 与 Delegate 模式 | `base::ObserverList` / `base::CheckedObserver` 的安全观察者模式(检查观察者是否已被销毁);`base::ScopedObservation` 的 RAII 注册/反注册;Delegate 模式在 Chrome 中的广泛应用:依赖注入、打破循环依赖、测试替身;`base::SupportsUserData` 的扩展点设计;这些模式如何用 Modern C++ 实现得比传统 GoF 更安全 | C++11 `std::function`,C++20 concepts | +| 07-02 | 22-factory-pimpl-testability.md | Factory、Pimpl 与可测试性架构 | Chrome 的 Factory 模式:异步/可能失败的构造(`Create()` 静态方法返回 `std::unique_ptr`);`base::NoDestructor`(懒初始化单例);`base::LazyInstance`(已废弃,讨论废弃原因);Pimpl 在 Chrome 中的使用;Abstract Base Class + Impl + Fake 的测试三层架构;如何将这些模式应用到自己的项目中提升可测试性 | C++11 `std::unique_ptr`,C++20 modules | + +## 练习与项目 + +### 文章末尾练习 +- 每篇 3-5 道,重点关注设计决策的权衡分析(而非纯代码实现) +- 包含"用标准 C++ 重写 Chrome 组件的简化版"练习 +- 包含"分析你自己的项目是否适用此模式"的思考题 + +### 实战项目 +1. **类型安全回调库**:参考 `OnceCallback`/`RepeatingCallback`,用 C++23 实现一个简化版的类型安全回调库,支持移动语义和弱引用取消 +2. **序列化任务调度器**:参考 Chrome 的 Sequence 概念,实现一个基于任务队列的调度器,支持优先级、延迟任务和 `PostTaskAndReply` 模式 +3. **缓存友好容器库**:实现 `flat_map`/`flat_set` 和 `small_map`,用 Google Benchmark 做性能对比测试,生成性能分析报告 + +## 可复用组件库规划 + +> 以下组件为独立于教学文章的可复用 C++23 组件库,由作者自行实现。组件库可被外部项目直接集成。 + +### 组件清单 + +| 组件 | 头文件 | 灵感来源 | 对应标准 | 说明 | +|------|--------|---------|---------|------| +| `once_callback` | `once_callback.hpp` | `base::OnceCallback` | C++23 `std::move_only_function` | 利用 deducing this 和 `std::move_only_function` 实现仅可调用一次的回调 | +| `repeating_callback` | `repeating_callback.hpp` | `base::RepeatingCallback` | `std::function` | 可重复调用的回调,支持拷贝 | +| `weak_ptr` | `weak_ptr.hpp` | `base::WeakPtr/WeakPtrFactory` | `std::weak_ptr`(语义不同) | 不依赖 shared_ptr 控制块,利用 `std::atomic` 和 `std::latch` 实现序列安全 | +| `scoped_generic` | `scoped_generic.hpp` | `base::ScopedGeneric` | `std::unique_ptr` + 自定义 deleter | 通用 RAII 守卫模板,利用 C++23 deducing this 简化链式调用 | +| `flat_map` | `flat_map.hpp` | `base::flat_map` | C++23 `std::flat_map` | 提供与 `std::flat_map` 一致的接口 + 额外扩展 | +| `small_map` | `small_map.hpp` | `base::small_map` | 无标准对应 | 利用 `std::is_constant_evaluated()` 优化编译期路径 | +| `circular_deque` | `circular_deque.hpp` | `base::circular_deque` | 无标准对应 | 分段连续存储,支持 `std::ranges` 接口 | +| `time_delta` | `time_delta.hpp` | `base::TimeDelta` | C++20 `std::chrono` calendar | 利用 C++23 `std::print` 格式化和 `std::chrono` 增强 | +| `check_macros` | `check.hpp` | `CHECK()/DCHECK()` | C++23 `std::stacktrace` + `std::unreachable()` | 利用 `std::stacktrace` 自动捕获调用栈 | +| `observer_list` | `observer_list.hpp` | `base::ObserverList/CheckedObserver` | 无标准对应 | 利用 C++23 `std::flat_map` 管理观察者列表 | + +### 设计原则 + +1. **Header-only 优先**:大多数组件为单头文件,include 即用 +2. **C++23 基准要求**:充分利用 `std::move_only_function`、`std::expected`、`std::flat_map`、`std::print`、`std::stacktrace`、deducing this、`std::unreachable()`、multidimensional `operator[]`、`std::ranges` 增强、`if consteval` 等特性 +3. **向后兼容可选**:通过 `#if __cplusplus` 条件编译提供 C++20 降级路径(非必需) +4. **零外部依赖**:不依赖 Chrome 源码、Abseil、Boost 或任何第三方库 +5. **单元测试覆盖**:每个组件配备 Catch2/Google Test 测试文件 +6. **性能基准**:关键组件(容器、回调)配备 Google Benchmark 对比测试 +7. **CMake 集成**:可通过 `add_subdirectory` 或 `FetchContent` 集成到外部项目 + +### 目录结构 + +``` +documents/vol9-open-source-project-learn/chrome/components/ +├── CMakeLists.txt # 顶层 CMake(可选 add_subdirectory) +├── README.md # 组件库说明与集成方式 +├── include/ +│ └── chrome_learn/ +│ ├── once_callback.hpp +│ ├── repeating_callback.hpp +│ ├── weak_ptr.hpp +│ ├── scoped_generic.hpp +│ ├── flat_map.hpp +│ ├── small_map.hpp +│ ├── circular_deque.hpp +│ ├── time_delta.hpp +│ ├── check.hpp +│ └── observer_list.hpp +├── tests/ +│ ├── CMakeLists.txt +│ ├── test_once_callback.cpp +│ ├── test_weak_ptr.cpp +│ ├── test_flat_map.cpp +│ └── ... +└── benchmarks/ + ├── CMakeLists.txt + ├── bench_flat_map.cpp + ├── bench_callback.cpp + └── ... +``` + +### 与文章的关系 + +- 文章(ch00-ch07)负责**讲解设计思路和原理** +- 组件库提供**可直接使用的代码实现** +- 每篇文章末尾指向对应组件的头文件和测试,作为"动手实践"环节 +- 组件库可独立于文章使用 — 读者可以直接 clone 组件目录集成到自己的项目 + +## Chrome→C++ 标准映射总览 + +| Chrome Base 组件 | C++ 标准对应 | 标准版本 | Chrome 先行优势 | +|-----------------|------------|---------|---------------| +| `base::OnceCallback` | `std::move_only_function` | C++23 | 移动语义、WeakPtr 集成、Unretained 保护 | +| `base::StringPiece` | `std::string_view` | C++17 | 提前约 5 年提供非拥有字符串视图 | +| `base::span` | `std::span` | C++20 | 提前约 7 年提供视图类型 | +| `base::flat_map/set` | `std::flat_map/set` | C++23 | 提前约 15 年的缓存友好关联容器 | +| `base::WeakPtr` | `std::weak_ptr`(语义不同) | C++11 | 不需要 `shared_ptr` 控制块,侵入式序列安全 | +| `scoped_refptr` | `std::shared_ptr` | C++11 | 侵入式引用计数减少堆分配 | +| `base::TimeDelta` | `std::chrono::duration` | C++11 | 更友好的 API,内置测试注入点 | +| `base::AutoLock` | `std::lock_guard` | C++11 | 等价,但配套线程限制注解 | +| `base::raw_ptr` | 无直接对应 | — | use-after-free 防护,C++ 标准无等价机制 | +| `base::TaskRunner` | 无直接对应(C++26 Senders 提案) | C++26? | 序列化任务调度模型远超 `std::async` | + +## 参考资料 + +### 官方文档 +- [Chromium 源码 base/ 目录](https://source.chromium.org/chromium/chromium/src/+/main:base/) +- [Chromium base/ README](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/base/README.md) +- [Chromium Callback 文档](https://chromium.googlesource.com/chromium/src/+/main/docs/callback.md) +- [Chromium Threading and Tasks 文档](https://chromium.googlesource.com/chromium/src/+/main/docs/threading_and_tasks.md) +- [Chromium base/memory/ README](https://chromium.googlesource.com/chromium/src/+/refs/heads/main/base/memory/README.md) +- [Chromium C++ Style Guide](https://chromium.googlesource.com/chromium/src/+/main/styleguide/c++/c++-features.md) +- [Chromium C++ Design Patterns](https://www.chromium.org/chromium-os/developer-library/reference/cpp/cpp-patterns/) +- [Chromium C++ 101 Codelab](https://www.chromium.org/developers/codelabs/cpp101/) +- [Chrome Sequence Manager 设计文档](https://source.chromium.org/chromium/chromium/src/+/main:base/task/sequence_manager/README.md) +- [Component Cookbook](https://www.chromium.org/developers/design-documents/cookbook/) +- [Multi-process Architecture](https://www.chromium.org/developers/design-documents/multi-process-architecture/) + +### 标准参考 +- [cppreference: std::move_only_function (C++23)](https://en.cppreference.com/w/cpp/utility/functional/move_only_function) +- [cppreference: std::flat_map (C++23)](https://en.cppreference.com/w/cpp/container/flat_map) +- [cppreference: std::stacktrace (C++23)](https://en.cppreference.com/w/cpp/header/stacktrace) + +### 社区分析 +- [浅析 RefCounted 和 WeakPtr: Chromium Base 篇](https://kingsamchen.github.io/2018/05/14/demystify-ref-counted-and-weak-ptr-in-chromium-base/) +- [Chromium MessageLoop and TaskScheduler](https://keyou.github.io/blog/2019/06/11/Chromium-MessageLoop-and-TaskScheduler/) +- [Chromium Base MessageLoop Internals](https://kingsamchen.github.io/2018/11/25/chromium-base-message-loop-internals-1/) +- [Deep Dive into Chromium: Comprehensive Analysis](https://medium.com/@threehappyer/deep-dive-into-chromium-a-comprehensive-analysis-from-architecture-design-to-core-code-8cc8d3a328e3)