惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

K
Kaspersky official blog
T
Threat Research - Cisco Blogs
N
News and Events Feed by Topic
Hacker News: Ask HN
Hacker News: Ask HN
Project Zero
Project Zero
Application and Cybersecurity Blog
Application and Cybersecurity Blog
博客园 - 叶小钗
Security Latest
Security Latest
Spread Privacy
Spread Privacy
aimingoo的专栏
aimingoo的专栏
N
News and Events Feed by Topic
Webroot Blog
Webroot Blog
U
Unit 42
Cyberwarzone
Cyberwarzone
小众软件
小众软件
Scott Helme
Scott Helme
Engineering at Meta
Engineering at Meta
Microsoft Security Blog
Microsoft Security Blog
T
The Blog of Author Tim Ferriss
A
About on SuperTechFans
爱范儿
爱范儿
S
Schneier on Security
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Schneier on Security
Schneier on Security
Latest news
Latest news
GbyAI
GbyAI
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Simon Willison's Weblog
Simon Willison's Weblog
The Register - Security
The Register - Security
WordPress大学
WordPress大学
博客园_首页
Blog — PlanetScale
Blog — PlanetScale
PCI Perspectives
PCI Perspectives
Jina AI
Jina AI
AI
AI
NISL@THU
NISL@THU
I
Intezer
G
GRAHAM CLULEY
B
Blog
S
Secure Thoughts
IT之家
IT之家
宝玉的分享
宝玉的分享
Recent Announcements
Recent Announcements
Y
Y Combinator Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
酷 壳 – CoolShell
酷 壳 – CoolShell
有赞技术团队
有赞技术团队
V2EX - 技术
V2EX - 技术
Recorded Future
Recorded Future
Hacker News - Newest:
Hacker News - Newest: "LLM"

I'm OWenT

国产大模型(GLM 5.1、Kimi K2.6)真实场景效果和 Coding Plan 额度测试 新版本libatapp的连接管理——从etcd服务发现到拓扑驱动的自动重连 新版本libatbus的设计变更——从树形路由到拓扑驱动 AI真好用-给Blog主题统一加mermaid,chart.js,excalidraw,draw.io的多种引入方式支持 给内网部署Squid-通用HTTP下载缓存 UE使用CodeChecker和clang-tidy生成静态分析报告 找出UE的循环依赖 C++小协程栈和临时变量及作用域的栈溢出问题分析 游戏服务的可观测性能力建设(C++生态) 指标上报的多线程优化和多拉取源点优化 协程(libcopp)的Channel功能和CPU命中率优化 通用RPC代码生成器 实现strong_rc_ptr(比shared_ptr更快的引用计数智能指针) 手夯一个STL allocator和对象内存分析组件 std::condition_variable 的信号丢失问题 踩坑一处(GCC)STL std::async 实现BUG导致的crash问题 GCC 14的一个warning to error BUG 给xresloader(Excel导表工具)增强UE读表支持(包含蓝图,Blueprint) Opentelemetry社区在gRPC的几个链接问题(静态库和动态库混用,musl工具链,符号裁剪) Excel转表工具(xresloader)的新验证器(验证外部Excel和文本数据,唯一性和自定义规则) protobuf v22和gRPC v1.55版本升级的依赖变化和upb适配 关于protobuf近期版本(v20/v3.20+)和 gRPC v1.54版本在某些编译环境下的一些链接和编译问题 xresloader-Excel导表工具链的近期变更汇总 打通游戏服务端框架的C++20协程改造的最后一环 Opentelemetry-cpp的Logs模块标准更新(涉及近期版本:1.8-1.9的BREAK CHANGES) 给cmake-toolset和工具链(curl等)加HTTP/2和HTTP/3支持 又开新坑之 coredns 插件: nftables和filter 关于opentelemetry-cpp社区对于C++ Head Only组件单例和符号可见性的讨论小记 填个转表工具 xresloader 去年的坑(数组尾部裁剪) 集成 upb 和 lua binding 的踩坑小记 libcopp对C++20协程的接入和接口设计 再度优化GCC、LLVM、Clang、libc++、libc++abi等套件的构建脚本 游戏服务的分布式事务优化(二)- 事务管理 游戏服务的分布式事务优化(一)- Write Ahead Log(WAL) 模块 记录一些bazel适配用编译选项 测试现代化硬件C++浮点数性能和一致性 适配Boringssl和OpenSSL 3.0 近期cmake-toolset的一些适配问题 C++20 Text Formatting/fmtlib 适配问题小记 再次重构LLVM+Clang+libcxx+libc++abi+其他相关工具的构建流程 重构基于CMake的构建工具链 新版GCC和LLVM+Clang终于Release啦 折腾一下nftables下的双拨 [C++20] Module partitions和符号交叉引用(声明和实现分离) [Rust] 实现一个线程安全且迭代器可以保存的链表 基于protobuf的代码生成 几个使用protobuf中C++接口的Arena的坑 Amazon Aurora DB存储引擎论文阅读小记 近期对libatapp的一些优化调整(增加服务发现和连接管理,支持yaml等) xresloader转表工具链增加了一些新功能(map,oneof支持,输出矩阵,基于模板引擎的加载代码生成等) 在游戏服务器中使用分布式事务 libcopp接入C++20 Coroutine和一些过渡期的设计 libatbus 的大幅优化 nftables初体验 容器配置开发环境小计 PALM Tree - 适合多核并发架构的B+树 - 论文阅读小记 跨平台协程库 - libcopp 简介 C++20 Coroutine 性能测试 (附带和libcopp/libco/libgo/goroutine/linux ucontext对比) 尝鲜Github Action 一些xresloader(转表工具)的改进 protobuf、flatbuffer、msgpack 针对小数据包的简单对比 协程框架(libcopp) 小幅优化 Excel转表工具(xresloader) 增加protobuf插件功能和集成 UnrealEngine 支持 Anna(支持任意扩展和超高性能的KV数据库系统)阅读笔记 C++20 Coroutine libcopp merge boost.context 1.69.0 Google去中心化分布式系统论文三件套(Percolator、Spanner、F1)读后感 Rust玩具-企业微信机器人通用服务 使用ELK辅助监控开发测试环境服务质量和问题定位 2018年的新通用伪随机数算法(xoshiro / xoroshiro)的C++(head only)实现 Webpack+vue+boostrap+ejs构建Web版GM工具 Rust的第二次接触-写个小服务器程序 理解和适配AEAD加密套件 atsf4g-co的进化:协程框架v2、对象路由系统和一些其他细节优化 协程框架(libcopp)v2优化、自适应栈池和同类库的Benchmark对比 可执行文件压缩 初识Rust 使用restructedtext编写xresloader文档 atframework的etcd模块化重构 C++的backtrace ECDH椭圆双曲线(比DH快10倍的密钥交换)算法简介和封装 protobuf-net的动态Message实现 pbc的proto3接入 atgateway内置协议流程优化-加密、算法协商和ECDH 整理一波软件源镜像同步工具+DevOps工具 Blog切换到Hugo libcopp v2的第一波优化完成 libcopp(v2) vs goroutine性能测试 libcopp的线程安全、栈池和merge boost.context 1.64.0 GCC 7和LLVM+Clang+libc++abi 4.0的构建脚本 libatbus的几个藏得很深的bug 用cmake交叉编译到iOS和Android 开源项目得一些小维护 atapp的c binding和c#适配 对象路由系统设计 2016年总结 近期的一个协程流程BUG 重写了llvm+clang+libc++和libc++abi的构建脚本 atsf4g完整游戏工程示例|I'm OWenT atframework基本框架已经完成|I'm OWenT
Protobuf又一坑 - C++标准和ABI兼容性
owent · 2026-01-11 · via I'm OWenT

blog-website

背景

前段时间例行升级我为游戏框架写的构建系统 cmake-toolset 时,又遇到了 protobuf 的一个新 ABI 兼容性问题。

这已经不是第一次被 protobuf 的 ABI 兼容性问题“教育”了。这篇把问题拆开讲清楚,并给出一套可落地的规避方案,供遇到类似问题的同学参考。

  • 编译环境:Windows + Visual Studio 2026
  • Abseil-Cpp:20250512.1
  • Protobuf:v31.1(看起来目前最新的 v33.1 也有这个问题)

现象与错误定位

首先在链接阶段出现错误:

[build] test_pb.pb.obj : error LNK2001: 无法解析的外部符号 "class google::protobuf::internal::GlobalEmptyStringConstexpr const google::protobuf::internal::fixed_address_empty_string" (?fixed_address_empty_string@internal@protobuf@google@@3VGlobalEmptyStringConstexpr@123@B) [D:\workspace\git\github\atframework\cmake-toolset\test\build_jobs_dir\cmake-toolset-test.vcxproj]
[build] D:\workspace\git\github\atframework\cmake-toolset\test\build_jobs_dir\bin\Debug\cmake-toolset-test.exe : fatal error LNK1120: 1 个无法解析的外部命令 [D:\workspace\git\github\atframework\cmake-toolset\test\build_jobs_dir\cmake-toolset-test.vcxproj]

我也在 opentelemetry-cpp 社区提了一个同样现象的 issue(见 open-telemetry/opentelemetry-cpp#3799)。

为了说明问题,我们把“生成代码”和“protobuf 库本体”的相关片段并排看一下。

生成的 .pb.cc

生成的 .pb.cc 里会出现类似这样的代码(把 std::string 的默认值初始化为一个固定地址的空串):

inline constexpr EntityRef::Impl_::Impl_(
    ::_pbi::ConstantInitialized) noexcept
      : id_keys_{},
        description_keys_{},
        schema_url_(
            &::google::protobuf::internal::fixed_address_empty_string,
            ::_pbi::ConstantInitialized()),
        type_(
            &::google::protobuf::internal::fixed_address_empty_string,
            ::_pbi::ConstantInitialized()),
        _cached_size_{0} {}

Protobuf 的定义(port.h / port.cc

protobuf 的 port.h 里有如下定义(简化摘录):

class alignas(8) GlobalEmptyStringConstexpr {
 public:
  const std::string& get() const { return value_; }
  // Nothing to init, or destroy.
  std::string* Init() const { return nullptr; }

  template <typename T = std::string, bool = (T(), true)>
  static constexpr std::true_type HasConstexprDefaultConstructor(int) {
    return {};
  }
  static constexpr std::false_type HasConstexprDefaultConstructor(char) {
    return {};
  }

 private:
  std::string value_;
};

using GlobalEmptyString = std::conditional_t<
    GlobalEmptyStringConstexpr::HasConstexprDefaultConstructor(0),
    const GlobalEmptyStringConstexpr, GlobalEmptyStringDynamicInit>;

PROTOBUF_EXPORT extern GlobalEmptyString fixed_address_empty_string;

对应的 port.cc 里会定义这个全局对象:

PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT
    PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 GlobalEmptyString
        fixed_address_empty_string{};

根因:C++20 触发了类型/符号差异

查阅 C++ 文档(例如 https://en.cppreference.com/w/cpp/string/basic_string/basic_string.html)可以看到:std::string 的构造函数是在 C++20 之后才逐步具备 constexpr 能力。

这意味着:

  • 当 protobuf 以 C++20(或更高) 编译时,GlobalEmptyStringConstexpr::HasConstexprDefaultConstructor(0) 可能成立,GlobalEmptyString 会走到 GlobalEmptyStringConstexpr 路径。
  • 当 protobuf 以 C++17(或更低) 编译时,上述条件不成立,GlobalEmptyString 会走到另一条类型分支。

而如果你的工程里出现了“protobuf 库”和“.pb.cc”采用不同 C++ 标准编译,且跨越了 C++20 这条分界线,那么:

  • 生成代码里引用的符号签名(mangled name)会以 GlobalEmptyStringConstexpr 为前提;
  • 但库里实际导出的符号却来自另一条类型分支;

最终就会出现前面那种链接错误(LNK2001)。

解决方法

一句话:不要让 “protobuf 库” 和 “生成的 .pb.cc” 在 C++20 前后混用

最理想的做法当然是全工程统一 C++ 标准(例如都用 C++20),并用同一套工具链把 protobuf/abseil 也一起重编。

但现实里经常会遇到“依赖是预编译包”的情况,这时就需要在 CMake 层面尽量推断 protobuf 的“实际编译标准”,并把 .pb.cc 的编译标准对齐到不会产生 ABI 差异的范围。

从 CMake 目标信息推断(第一版)

如果 protobuf 是通过 CMake package 导入的,导出的目标里一般会带 INTERFACE_COMPILE_FEATURES,例如:

set_target_properties(protobuf::libprotobuf PROPERTIES
  INTERFACE_COMPILE_FEATURES "cxx_std_17"
  INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"
  INTERFACE_LINK_LIBRARIES "\$<LINK_ONLY:ZLIB::ZLIB>;absl::absl_check;absl::absl_log;absl::algorithm;absl::base;absl::bind_front;absl::bits;absl::btree;absl::cleanup;absl::cord;absl::core_headers;absl::debugging;absl::die_if_null;absl::dynamic_annotations;absl::flags;absl::flat_hash_map;absl::flat_hash_set;absl::function_ref;absl::hash;absl::if_constexpr;absl::layout;absl::log_initialize;absl::log_globals;absl::log_severity;absl::memory;absl::node_hash_map;absl::node_hash_set;absl::optional;absl::random_distributions;absl::random_random;absl::span;absl::status;absl::statusor;absl::strings;absl::synchronization;absl::time;absl::type_traits;absl::utility;absl::variant;\$<LINK_ONLY:utf8_range::utf8_validity>"
)

我们可以读取 INTERFACE_COMPILE_FEATURES,尝试获得 cxx_std_NN 并用于约束生成的 proto targets。

我在 cmake-toolset 里原本就有一个 “patch protobuf 生成代码 target” 的接口(主要用来屏蔽 protobuf 生成代码触发的严格告警)。这次我把它扩展为:如果能推断出 protobuf 的 C++ 标准,就显式把生成的 .pb.cc target 对齐过去。

function(project_build_tools_patch_protobuf_targets)
  # 如果之前找到了 cxx_std_NN 的 INTERFACE_COMPILE_FEATURES, 就把 ATFRAMEWORK_CMAKE_TOOLSET_THIRD_PARTY_PROTOBUF_CXX_STANDARD 设置成这个标准数字部分
  if(ATFRAMEWORK_CMAKE_TOOLSET_THIRD_PARTY_PROTOBUF_CXX_STANDARD)
    foreach(PROTO_TARGET ${ARGN})
      set_target_properties(${PROTO_TARGET} PROPERTIES CXX_STANDARD
                                                       ${ATFRAMEWORK_CMAKE_TOOLSET_THIRD_PARTY_PROTOBUF_CXX_STANDARD})
    endforeach()

    if(MSVC)
      set(__additional_cxx_standard "/std:c++${ATFRAMEWORK_CMAKE_TOOLSET_THIRD_PARTY_PROTOBUF_CXX_STANDARD}")
    else()
      set(__additional_cxx_standard "-std=c++${ATFRAMEWORK_CMAKE_TOOLSET_THIRD_PARTY_PROTOBUF_CXX_STANDARD}")
    endif()
  endif()
  foreach(PROTO_TARGET ${ARGN})
    set(PROTO_TARGET_OPTIONS_CHANGED FALSE)
    get_target_property(PROTO_TARGET_OPTIONS ${PROTO_TARGET} COMPILE_OPTIONS)
    if(PROTO_TARGET_OPTIONS)
      set(__need_cxx_standard TRUE)
      if(NOT ATFRAMEWORK_CMAKE_TOOLSET_THIRD_PARTY_PROTOBUF_CXX_STANDARD
         OR PROTO_TARGET_OPTIONS MATCHES
            "(/std:|-std=)(c|gnu)\\+\\+${ATFRAMEWORK_CMAKE_TOOLSET_THIRD_PARTY_PROTOBUF_CXX_STANDARD}")
        set(__need_cxx_standard FALSE)
      else()
        if(MSVC)
          string(REGEX REPLACE "/std:c\\+\\+[0-9a-zA-Z_]+" "" PROTO_TARGET_OPTIONS "${PROTO_TARGET_OPTIONS}")
        endif()
        string(REGEX REPLACE "-std=(c|gnu)\\+\\+[0-9a-zA-Z_]+" "" PROTO_TARGET_OPTIONS "${PROTO_TARGET_OPTIONS}")
        set(__need_cxx_standard TRUE)
      endif()

      # 我们原来有一些根据编译环境生产的Patch编译选项,放在 PROJECT_BUILD_TOOLS_PATCH_PROTOBUF_SOURCES_OPTIONS ,这里是做融合
      foreach(TEST_OPTION ${PROJECT_BUILD_TOOLS_PATCH_PROTOBUF_SOURCES_OPTIONS})
        if(NOT "${TEST_OPTION}" IN_LIST PROTO_TARGET_OPTIONS)
          list(APPEND PROTO_TARGET_OPTIONS "${TEST_OPTION}")
          set(PROTO_TARGET_OPTIONS_CHANGED TRUE)
        endif()
      endforeach()
      if(__need_cxx_standard)
        list(APPEND PROTO_TARGET_OPTIONS "${__additional_cxx_standard}")
        set(PROTO_TARGET_OPTIONS_CHANGED TRUE)
      endif()
    else()
      set(PROTO_TARGET_OPTIONS ${PROJECT_BUILD_TOOLS_PATCH_PROTOBUF_SOURCES_OPTIONS})
      if(__additional_cxx_standard)
        list(APPEND PROTO_TARGET_OPTIONS "${__additional_cxx_standard}")
      endif()
      set(PROTO_TARGET_OPTIONS_CHANGED TRUE)
    endif()
    if(PROTO_TARGET_OPTIONS_CHANGED)
      set_target_properties(${PROTO_TARGET} PROPERTIES COMPILE_OPTIONS "${PROTO_TARGET_OPTIONS}")
    endif()
  endforeach()
endfunction()

不过把这个策略落下去之后,又遇到了两个“二次踩坑”。这俩坑的共同点是:仅凭 cxx_std_NN 并不足以推断“实际在用的编译标准/ABI约束”

新问题一:cxx_std_NN 可能不准确

上游工程实际使用了 C++17:

C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.44.35207\include\string_view(12): warning STL4038: The contents of <string_view> are available only with C++17 or later. [D:\a\opentelemetry-cpp\opentelemetry-cpp\build\opentelemetry_proto.vcxproj]
  (compiling source file '../../../../generated/third_party/opentelemetry-proto/opentelemetry/proto/common/v1/common.pb.cc')
      C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.44.35207\include\vector(29,1):
      see declaration of 'std'
  
D:\a\opentelemetry-cpp\opentelemetry-cpp\tools\vcpkg\installed\x64-windows\include\absl\strings\string_view.h(53,26): error C2061: syntax error: identifier 'string_view' [D:\a\opentelemetry-cpp\opentelemetry-cpp\build\opentelemetry_proto.vcxproj]
  (compiling source file '../../../../generated/third_party/opentelemetry-proto/opentelemetry/proto/common/v1/common.pb.cc')

但 protobuf 导出的 package 却声称自己是 cxx_std_14 (某些老一些的版本):

get_target_property(protobuf_lib_compile_features protobuf::libprotobuf INTERFACE_COMPILE_FEATURES)
if(protobuf_lib_compile_features MATCHES "(^|\\s)cxx_std_([0-9]+)(|$\\s)")
  message(
    STATUS
      "protobuf::libprotobuf detected compile features cxx_std_${CMAKE_MATCH_2}."
  )
endif()

输出是 -- protobuf::libprotobuf detected compile features cxx_std_14.

所以这里的处理思路是:不要完全依赖 cxx_std_NN,而是把 C++20 作为 ABI 分界线

如果你的工程整体用的是 C++20(或更高),而导入的 protobuf “看起来”低于 C++20,那么就需要避免让 .pb.cc 跨过 C++20;反过来也同理。

if(DEFINED CMAKE_CXX_STANDARD AND protobuf_lib_compile_features MATCHES
                                  "(^|\\s)cxx_std_([0-9]+)(|$\\s)")
  # Whether the protobuf library using C++20 or higher may have different
  # ABI. We need to make sure our proto targets are using the same C++
  # standard.
  if(${CMAKE_MATCH_2} LESS 20 AND CMAKE_CXX_STANDARD GREATER_EQUAL 20)
    # Some versions of protobuf will set cxx_std_14, but actually require
    # c++17
    set(protobuf_lib_compile_features_cxx_std 17)
  elseif(${CMAKE_MATCH_2} GREATER_EQUAL 20 AND CMAKE_CXX_STANDARD LESS 20)
    set(protobuf_lib_compile_features_cxx_std ${CMAKE_MATCH_2})
  endif()
  message(
    STATUS
      "protobuf::libprotobuf detected compile features cxx_std_${CMAKE_MATCH_2}."
  )
endif()

新问题二:abseil-cpp 如果以 C++20 配置/编译,会出现“配置宏不匹配”

在 Linux + GCC 上,如果你把 .pb.cc 强行降到 C++17,可能会遇到如下编译错误:

[  2%] Building CXX object CMakeFiles/opentelemetry_proto.dir/generated/third_party/opentelemetry-proto/opentelemetry/proto/resource/v1/resource.pb.cc.o
In file included from /usr/local/include/absl/numeric/int128.h:41,
                 from /usr/local/include/absl/strings/internal/str_format/arg.h:35,
                 from /usr/local/include/absl/strings/str_format.h:83,
                 from /usr/local/include/absl/crc/crc32c.h:32,
                 from /usr/local/include/absl/crc/internal/crc_cord_state.h:23,
                 from /usr/local/include/absl/strings/cord.h:80,
                 from /usr/local/include/google/protobuf/io/coded_stream.h:111,
                 from /home/runner/build/generated/third_party/opentelemetry-proto/opentelemetry/proto/resource/v1/resource.pb.h:20,
                 from /home/runner/build/generated/third_party/opentelemetry-proto/opentelemetry/proto/resource/v1/resource.pb.cc:6:
/usr/local/include/absl/types/compare.h:60:12: error: ‘partial_ordering’ has not been declared in ‘std’
   60 | using std::partial_ordering;
      |            ^~~~~~~~~~~~~~~~
/usr/local/include/absl/types/compare.h:61:12: error: ‘strong_ordering’ has not been declared in ‘std’
   61 | using std::strong_ordering;
      |            ^~~~~~~~~~~~~~~
/usr/local/include/absl/types/compare.h:62:12: error: ‘weak_ordering’ has not been declared in ‘std’
   62 | using std::weak_ordering;
      |            ^~~~~~~~~~~~~
/usr/local/include/absl/types/compare.h:455:56: error: ‘weak_ordering’ in namespace ‘absl’ does not name a type
  455 | constexpr bool compare_result_as_less_than(const absl::weak_ordering r) {
      |                                                        ^~~~~~~~~~~~~
/usr/local/include/absl/types/compare.h:470:17: error: ‘weak_ordering’ in namespace ‘absl’ does not name a type
  470 | constexpr absl::weak_ordering compare_result_as_ordering(const Int c) {

这里涉及 Three-way comparison(C++20 引入的 <compare>std::weak_ordering 等类型)。当 abseil 在安装/打包阶段被“按 C++20 可用”来配置过,而你的 .pb.cc 又用 C++17 编译时,就会出现这种 “头文件里 using std::weak_ordering; 但标准库并未提供” 的报错。

这也说明了:即便你只想“把 .pb.cc 降回 C++17 来贴合 protobuf 的 ABI”,也必须确保 abseil 的配置/编译假设不会被打破。

此外还有一个容易误判的点:一些 protobuf 版本会写 target_compile_features("${target}" PUBLIC cxx_std_17),但如果你在构建 protobuf 时设置了 -DCMAKE_CXX_STANDARD=20,那么它实际可能会用 C++20 编译,可导出的 package 信息仍然停留在 cxx_std_17。这会让“靠 package 元信息推断 ABI”变得更不可靠。

因此,更稳妥(但成本更高)的思路是:在工具链层面做一个小探针,分别用 -std=c++17-std=c++20(或 MSVC 对应的 /std:c++XX)尝试编译/链接一个最小样例,判断当前安装的 protobuf/abseil 组合到底要求哪个标准。

#include <iostream>
#include <google/protobuf/message.h>
int main () {
  std::cout<< ::google::protobuf::internal::fixed_address_empty_string.get()<< std::endl;
  return 0;
}

当然实际情况还和 protobuf/abseil 具体版本、打包方式、编译器/标准库实现有关,落地时要保留余地。

不过实际上,在Linux+GCC 13里,protobuf用C++17,而 .pb.cc 使用C++20是没问题的。因为这样虽然符号表里有非 constexpr 的分支,但是编译 .pb.cc 的时候,整个 fixed_address_empty_string 走编译期计算了。 因为 constexpr 并不是强制性的编译期评估( consteval 才是 ),所以如果依赖这个行为的话会和编译环境、编译器版本相关。而恰好MSVC没有全部走编译期计算,所以触发了这个问题。

官方的临时解决方案

截至写作时间,最新 protobuf 主干分支已经把相关逻辑改成了下面这样:在 MSVC 下强制禁用 constexpr std::string 的那条优化路径,从而规避跨标准导致的符号不一致问题。

这类改动在发布版落地之前,可以视作一个“临时止血方案”(前提是你能升级到包含该修复的版本/提交,或在自维护分支里 backport)。

// Take advantage of C++20 constexpr support in std::string.
class alignas(8) GlobalEmptyStringConstexpr {
 public:
  const std::string& get() const { return value_; }
  // Nothing to init, or destroy.
  std::string* Init() const { return nullptr; }

  // Disable the optimization for MSVC and Xtensa.
  // There are some builds where the default constructed string can't be used as
  // `constinit` even though the constructor is `constexpr` and can be used
  // during constant evaluation.
#if !defined(_MSC_VER) && !defined(__XTENSA__)
  // Compilation fails on Xtensa: b/467129751
  template <typename T = std::string, bool = (T(), true)>
  static constexpr std::true_type HasConstexprDefaultConstructor(int) {
    return {};
  }
#endif
  static constexpr std::false_type HasConstexprDefaultConstructor(char) {
    return {};
  }

 private:
  std::string value_;
};

最后

Google 生态下 protobuf 的坑确实不少。如果你对我之前踩过的其他 protobuf 相关坑感兴趣,可以直接在 Blog 里搜关键词 protobuf

欢迎小伙伴们交流拍砖。