公众号
imgsrc="https://wanghenshui.github.io/cppweeklynews/assets/code.png"alt=""width="30%"
点击「查看原文」跳转到 GitHub 上对应文件,链接就可以点击了
qq群 753792291 答疑在这里
欢迎投稿,推荐或自荐文章/软件/资源等,评论区留言
一月邮件 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/#mailing2026-01
标准委员会动态/ide/编译器信息放在这里
编译器信息最新动态推荐关注hellogcc公众号 本周更新 2025-01-08 第288期
想要128位整数但不想用编译器扩展?自己实现一个,性能还和 __uint128_t一样!
核心思路很简单:用两个64位limb表示,就像两位数的base 2^64数:
structu128 {
u64low=0;
u64high=0;
};
加法 - 用carry intrinsic:
u128operator+(u128 a, u128 b) {
u128r;
unsignedcharc=_addcarry_u64(0, a.low, b.low, &r.low);
(void)_addcarry_u64(c, a.high, b.high, &r.high);
returnr;
}
生成的汇编完美:
mov rax, rdi
add rax, rdx
adc rsi, rcx ; add with carry
mov rdx, rsi
ret
减法同理用 _subborrow_u64,一条 sub+一条 sbb搞定。
乘法 - 利用BMI2的_mulx_u64:
展开 (a.low + 2^64·a.high) × (b.low + 2^64·b.high) 后,只有三项贡献低128位:
u128operator*(u128 a, u128 b) {
u128r;
u64p0_hi;
r.low=_mulx_u64(a.low, b.low, &p0_hi);
u64t1_hi, t2_hi;
u64t1_lo=_mulx_u64(a.low, b.high, &t1_hi);
u64t2_lo=_mulx_u64(a.high, b.low, &t2_hi);
r.high=p0_hi+t1_lo+t2_lo; // carry会被丢弃,正好
returnr;
}
比较 - 用borrow技巧:
a < b 等价于 a - b 产生borrow:
booloperator<(u128 a, u128 b) {
u64dont_care;
unsignedcharborrow=_subborrow_u64(0, a.low, b.low, &dont_care);
borrow=_subborrow_u64(borrow, a.high, b.high, &dont_care);
returnborrow!=0;
}
汇编只有四条指令:cmp, sbb, setb, ret,完美!
固定宽度big integer的优势:性能可预测,没有动态分配,完全内联。生产环境用256位、512位都是这个套路扩展。比那些通用big integer库快多了,因为编译器能把一切都优化成直线指令。
Qt hackathon项目:用C++26反射让QRangeModel直接支持plain C++类,不需要Q_OBJECT宏!
目标:让这样的plain struct自动变成QAbstractItemModel:
structEntry {
QStringname;
doublevalue;
};
用于QML时自动有name和value两个role,用于TableView时自动两列。不需要moc,不需要tuple协议boilerplate。
C++26三大新语法:
1.反射操作符 ^^ - 获取类型的 std::meta::info
2.splice操作符 [: :] - 把反射信息注入代码
3.注解 [[=...]] - 比attribute更灵活
核心实现 - 计算成员数量:
staticconstevalautoelement_count() {
returnnonstatic_data_members_of(^^T,
std::meta::access_context::current()).size();
}
^^T 反射类型T,nonstatic_data_members_of 返回成员列表,.size() 获取数量。全是编译期!
获取列名:
staticQVariantcolumn_name(int section) {
QVariantresult;
QtPrivate::applyIndexSwitch<element_count()>(section, [&](auto idxConstant) {
constexprautomember=nonstatic_data_members_of(^^T,
std::meta::access_context::current()).at(idxConstant.value);
result=QString::fromUtf8(u8identifier_of(member).data());
});
returnresult;
}
用 applyIndexSwitch生成编译期switch,u8identifier_of获取成员名称。
读取成员值 - splice大显身手:
staticQVariantreadRole(constT&item, int role) {
constintindex=role-Qt::UserRole;
QVariantresult;
QtPrivate::applyIndexSwitch<element_count()>(index, [&](auto idxConstant){
constexprautomember=nonstatic_data_members_of(^^T,
std::meta::access_context::current()).at(idxConstant.value);
result=item.[:member:]; // splice!等价于item.name或item.value
});
returnresult;
}
item.[:member:] 这个splice操作把反射信息注入成实际的成员访问!
写入成员:
staticboolwriteRole(T&item, constQVariant&data, int role) {
QtPrivate::applyIndexSwitch<element_count()>(index, [&](auto idxConstant){
constexprautomember=nonstatic_data_members_of(^^T, ...).at(idxConstant.value);
usingMemberType= [:type_of(member):]; // splice type
result=data.canConvert<MemberType>();
if (result)
item.[:member:] =data.value<MemberType>();
});
returnresult;
}
[:type_of(member):] splice类型,item.[:member:] splice访问。
支持带getter/setter的类:
用注解标记property:
class [[=QRangeModel::RowCategory::MultiRoleItem]] Class {
public:
[[=Qt::Property{}]] QStringname() const { returnm_name; }
voidsetName(QString name) { m_name=name; }
private:
QStringm_name;
};
通过 annotations_of_with_type筛选property getter:
staticconstevalboolis_property_getter(std::meta::info member) {
returnis_const(member) &&is_function(member)
&&annotations_of_with_type(member, ^^Qt::Property).size();
}
找setter就用字符串匹配”set”+首字母大写:
staticconstevalstd::optional<std::meta::info> property_setter(std::size_t idx) {
autogetter=property_getter(idx);
autoproperty_name=identifier_of(*getter);
constautoset_prefix=std::string("set");
for (std::meta::infomember : members_of(^^T, ...)) {
if (has_identifier(member) &&is_function(member) &&!is_const(member)) {
autosetter_name=set_prefix+property_name;
setter_name[3] -=' '; // poor-man's uppercase
if (identifier_of(member) ==setter_name)
returnmember;
}
}
returnstd::nullopt;
}
结论:
C++26反射让我们能写出”看起来像魔法”的代码,不需要moc,不需要宏,类型安全还全是编译期。gcc trunk已经支持,clang应该很快跟上。等C++26正式发布(2026年3月),这些都能用到生产环境了!
能用反射替代moc吗?大部分可以,最大挑战可能是 signals:/slots:区块,可能需要逐个函数加注解。
Chromium的C++特性管理手册,标准委员会的事是他们的事,能不能用是我们说了算。
版本支持时间线:
新标准刚出来不是马上能用的,要等工具链支持,然后还要过两年TBD期。如果TBD期满还没决定,默认允许(没理由就不禁)。
一些有趣的禁止项:
inline namespace - Google风格禁止,和component系统不兼容
long long - 直接禁止,用 int64_t
User-Defined Literals - 12_km 这种?禁了
<chrono> - 禁止!和 base/time重叠。C++20时区功能再好,Chromium有自己的时间库
<regex> - 禁止,用 third_party/re2
std::function, std::bind - 禁止,有 base::Callback
std::shared_ptr, std::weak_ptr - 禁止,用 base::RefCounted和 base::WeakPtr
<thread>, <mutex>, <condition_variable> - 全禁,用 base/synchronization
char8_t - 禁止!因为所有API都用 char*,加 char8_t只会到处都是cast
std::span - 禁止,base::span功能更丰富
std::filesystem - 禁止(估计是有 base/files)
并行算法 - 禁止,libc++支持不完整,且要用Chrome自己的线程池
允许的C++20特性:
Concepts - 允许!
template <HashableT>
voidf(T) { ... }
consteval - 允许
Default comparisons和 <=> - 允许
friendautooperator<=>(constS&, constS&) =default;
Designated initializers - 允许
Ss{ .y=3 };
Ranges算法 - 允许
std::ranges::all_of(kArr, is_even);
[[likely]], [[unlikely]] - 允许
std::bit_cast - 禁止!允许cast掉const很危险,用 base::bit_cast
C++23的新东西:
std::expected - TBD(有 base::expected)
std::flat_map系列 - TBD(有 base::flat_map)
std::print - TBD(有LOG())
std::generator - TBD(coroutine还是TBD)
std::move_only_function - TBD(有 base::OnceCallback)
Abseil的情况:
大部分被禁:
-absl::string_view - 用 std::string_view
-absl::optional - 用 std::optional
-absl::Span - 用 base::span(更符合std)
-absl::btree_* - 禁!会显著增加代码体积
base/logging.h(但想迁移,没人做)base/strings(absl::StrFormat例外允许)base/synchronizationbase/time唯一例外:absl::StrFormat - 允许(比printf类型安全)
第三方库的豁免:
第三方库内部可以用禁止特性,但Chromium调用它们的接口时,禁止类型只能在边界转换:
// 第三方函数返回std::shared_ptr
autoresult=third_party_func();
automy_ptr=ConvertToBaseRefCounted(result); // 立刻转换
// 后续只用my_ptr
不允许std::shared_ptr扩散到Chromium代码里。
提案流程:
想解禁某个特性?发邮件到cxx@chromium.org,说明理由,附上讨论链接。如果达成共识,提交CR修改这个文档。
总之:Chromium不追新,稳定压倒一切,base库 > std库 > abseil。毕竟是几千万行代码的项目,牵一发动全身。
从GC语言转C++最难理解的就是ownership - 谁拥有这个对象?谁负责清理?
核心问题:
char*get_name(File* file);
返回的字符串是新分配的(调用者要free)还是file内部的(调用者不能动)?GC语言不用管,C++必须搞清楚。
RAII - 自动清理的魔法:
voidfoo(std::size_t buffer_size) {
std::unique_ptr<char[]> buffer=std::make_unique<char[]>(buffer_size);
intresult=0;
while (has_more) {
read(buffer.get());
result+=process(buffer.get());
}
returnresult; // buffer自动释放,即使有异常也不会泄漏
}
对比手动管理:
voidfoo(std::size_t buffer_size) {
char*buffer=newchar[buffer_size];
intresult=0;
try {
while (has_more) {
read(buffer);
result+=process(buffer);
}
} catch (std::exceptione) {
deletebuffer; // 异常路径要记得delete
throwe;
}
deletebuffer; // 正常路径也要delete
returnresult;
}
手动管理容易漏,RAII让编译器帮你管理。
Destructor怎么工作:
classRAIIMutex {
private:
std::mutex&mutex;
public:
RAIIMutex(std::mutex& m): mutex{m} {
mutex.lock();
}
~RAIIMutex() { // 析构函数
mutex.unlock();
}
};
voidfoo() {
RAIIMutexguard(global_variable_mutex);
process(global_variable);
// 离开scope自动unlock
}
析构函数在对象lifetime结束时自动调用,编译器会在scope末尾插入destructor调用。
Lifetime - 何时销毁:
局部变量的lifetime是它的声明block。但RAII不是GC:
intmain() {
Bb;
if (some_condition) {
std::vector<int> vec{1, 2, 3};
b.set_vec(vec);
}
b.bar(); // UB!vec已经销毁,b里的引用悬空
}
GC会延长vec的生命周期,RAII不会。reference的lifetime必须≤对象的lifetime。
Move - 偷资源:
vector扩容时要把元素从旧buffer复制到新buffer:
template <typenameT>
voidvector<T>::grow(size_t new_capacity) {
T*new_buffer=newT[new_capacity];
for (size_ti=0; i<this->size; ++i) {
new_buffer[i] =this->buffer[i]; // 复制!
}
delete[]old_buffer; // 旧的马上就删,复制纯浪费
}
用move避免复制:
for (size_ti=0; i<this->size; ++i) {
new_buffer[i] =std::move(this->buffer[i]); // 偷资源
}
std::move 本质是什么?就是个cast:
template<classT>
T&&move(T& t) {
returnstatic_cast<T&&>(t);
}
把lvalue reference转成rvalue reference。rvalue reference是”这个对象要死了,可以偷它资源”的标记。
string的两个构造函数:
std::string(conststd::string&other); // 复制
std::string(std::string&&other); // 偷资源
第一个会分配新buffer并复制,第二个直接偷other的buffer指针,把other的指针设为nullptr。
总结:
C++不是GC,compiler不会帮你延长lifetime,必须手动保证reference有效。但RAII能让你不用手动free/delete,降低了一半复杂度。
C++20之前处理时区只能靠第三方库(多数用Howard Hinnant的date库,也是std::chrono的基础)。C++20终于标准化了时区支持!
为什么时区这么难:
核心概念:
系统时间(sys_time)<-> 本地时间(local_time)互转
sys_time基于Unix time(从1970-01-01起的秒数),忽略闰秒。虽然严格来说Unix time不等于UTC(差了累积的闰秒),但实际应用中够用了。
Unix Time vs UTC:
utc_clock基本用法:
constauto&timezone_db=std::chrono::get_tzdb();
constauto*riviera_tz=timezone_db.locate_zone("Europe/Paris");
conststd::chrono::sys_timenow=std::chrono::system_clock::now();
conststd::chrono::local_timelocal_now=riviera_tz->to_local(now);
conststd::chrono::sys_timereconverted=riviera_tz->to_sys(local_now);
std::cout<<"sys_time: "<<now<<'\n'; // UTC-like
std::cout<<"local: "<<local_now<<'\n'; // 巴黎当地时间
std::cout<<"reconverted: "<<reconverted<<'\n'; // 转回sys_time
get_tzdb() 获取时区数据库,locate_zone() 查找时区(会throw如果不存在),to_local()/to_sys() 互转。
转换会自动处理:
zoned_time - 时间+时区:
传裸time_point容易出错,zoned_time把时间和时区绑定:
std::chrono::zoned_timemeeting{"Europe/Paris", std::chrono::system_clock::now()};
这样API边界清晰,不会误用。如果传错时区名也会throw。
处理本地civil time:
local_t 是个pseudo-clock,表示没有指定时区的本地时间:
std::chrono::local_timemeeting=std::chrono::local_days{2025y/3/30} +9h;
这个时间要配合时区才有意义:
constauto&timezone_db=std::chrono::get_tzdb();
constauto*riviera_tz=timezone_db.locate_zone("Europe/Paris");
conststd::chrono::zoned_timezoned_meeting{riviera_tz, meeting};
std::cout<<"local time: "<<meeting<<'\n'; // 2025-03-30 09:00:00
std::cout<<"zoned time: "<<zoned_meeting<<'\n'; // 2025-03-30 09:00:00 CEST
std::cout<<"system time: "<<riviera_tz->to_sys(meeting) <<'\n'; // 2025-03-30 07:00:00
local_time关联时区后,库能正确处理:
最佳实践:
和自定义clock配合:
自定义clock定义”时间如何推进”,时区定义”人类如何解读这个时刻”。职责分离,抽象清晰。
总结:
C++20不假装时间简单,它给了你正确处理civil time的工具。如果代码和人类交互、调度事件、显示日期,时区不再是optional - 而且现在是标准库一部分了!
Raymond Chen写了一组文章讲如何用Windows的Active Accessibility API定位当前焦点元素。这是”The Old New Thing”风格的底层Windows编程。
目标:把鼠标移到当前focus的位置。
先用 GetGUIThreadInfo获取焦点信息:
GUITHREADINFOinfo= { sizeof(GUITHREADINFO) };
if (GetGUIThreadInfo(0, &info)) {
if (info.flags&GUI_CARETBLINKING) {
// 有caret(光标),直接用它的位置
MapWindowPoints(info.hwndCaret, nullptr, (POINT*)&info.rcCaret, 2);
SetCursorPos(info.rcCaret.right-1, info.rcCaret.bottom-1);
return;
}
// 没有caret,查找focus item
}
如果没有caret,用Active Accessibility找focus:
if (info.hwndFocus!=nullptr) {
wil::com_ptr_nothrow<IAccessible> acc;
// 先试试有没有自定义caret
if (SUCCEEDED(AccessibleObjectFromWindow(info.hwndFocus, OBJID_CARET,
IID_PPV_ARGS(acc.put()))) &&acc) {
// 用helper获取位置
}
// 再查focus item
if (SUCCEEDED(AccessibleObjectFromWindow(info.hwndFocus, OBJID_CLIENT,
IID_PPV_ARGS(acc.put()))) &&acc) {
wil::unique_variantvt;
if (acc->get_accFocus(&vt) ==S_OK) {
auto [childAcc, childId] =GetChild(acc.get(), vt);
// 用childAcc定位
}
}
}
GetChild helper处理Active Accessibility的child ID复杂逻辑:
std::tuple<wil::com_ptr_nothrow<IAccessible>, LONG>
GetChild(IAccessible*pacc, VARIANTconst&vt) {
if (vt.vt==VT_I4) {
wil::com_ptr_nothrow<IDispatch> disp;
if (vt.lVal!=CHILDID_SELF&&
SUCCEEDED(pacc->get_accChild(vt, disp.put())) &&disp) {
return { disp.try_query<IAccessible>(), CHILDID_SELF };
} else {
return { pacc, vt.lVal };
}
} elseif (vt.vt==VT_DISPATCH) {
return { wil::try_com_query_nothrow<IAccessible>(vt.pdispVal),
CHILDID_SELF };
}
return {};
}
发现quirk:File Explorer里横向滚动后,accLocation返回的是元素的未裁剪位置,鼠标会跑到窗口外面。
解决:裁剪到focus window的client area:
boolSetCursorPosToLocation(IAccessible* acc, LONG childId, HWND hwndClip) {
longx, y, cx, cy;
VARIANTvt= { VT_I4, childId };
if (acc->accLocation(&x, &y, &cx, &cy, vt) ==S_OK) {
RECTrcObject= { x, y, x+cx, y+cy };
RECTrcClient;
if (hwndClip&&GetClientRect(hwndClip, &rcClip)) {
MapWindowPoints(hwndClip, nullptr, (POINT*)&rcObject, 2);
if (IsWindow(hwndClip)) { // 防止窗口被销毁
IntersectRect(&rcObject, &rcObject, &rcClient);
}
}
SetCursorPos(rcObject.right-1, rcObject.bottom-1);
returntrue;
}
returnfalse;
}
但又有新问题:有些程序的focus window是0×0的dummy window(用来接收输入但不显示),裁剪太激进。
File Explorer的问题:focus item的parent不是Tiles view container而是wrapper,所以只裁剪一层不够。
解决:向上遍历整个父节点树,逐层裁剪:
boolSetCursorPosToLocation(IAccessible* acc, LONG childId) {
RECTrcObject;
if (GetAccessibleBounds(acc, childId, &rcObject)) {
RECTrcParent;
if (childId!=CHILDID_SELF) {
// 如果是child,先裁剪到直接parent
if (GetAccessibleBounds(acc, CHILDID_SELF, &rcParent)) {
IntersectRect(&rcObject, &rcObject, &rcParent);
}
} else {
// 如果是object本身,遍历所有parent
wil::com_ptr_nothrow<IAccessible> accParent(acc);
wil::com_ptr_nothrow<IDispatch> dispParent;
while (accParent->get_accParent(dispParent.put()) ==S_OK&&dispParent) {
accParent=dispParent.try_query<IAccessible>();
if (accParent&&
GetAccessibleBounds(accParent.get(), CHILDID_SELF, &rcParent)) {
IntersectRect(&rcObject, &rcObject, &rcParent);
}
}
}
SetCursorPos(rcObject.right-1, rcObject.bottom-1);
returntrue;
}
returnfalse;
}
现在工作得不错了。还有些程序完全不报告caret位置,但那是它们的问题。
总结:
这个系列展示了Windows编程的典型场景:API看起来简单,实际用起来到处是edge case。需要:
Raymond的风格是一步步发现问题、解释原因、提供解决方案,非常适合学习Windows API
Jason Turner介绍C++26的新特性:用户自定义static_assert消息
C++11引入static_assert时需要提供一个字符串字面量作为错误消息,C++17允许省略消息。C++26现在允许第二个参数是constexpr表达式,只要这个类型有 size()成员(可转换为size_t)和 data()成员(可转换为const char*)就行。
Jason演示了一个想法:用std::format生成动态错误消息。但问题是std::format还不是constexpr(可能C++26会加入)。不过fmtlib的format_to已经支持constexpr了。
他展示了一个helper函数:
constevalautoformat(auto fmt, auto... args) {
std::array<char, 1024> buffer{};
fmt::format_to(buffer.begin(), fmt, args...);
returnbuffer;
}
然后可以这样用:
template<autovalue>
voiddo_things() {
static_assert(value>5,
lefticus::format("Expected > 5, got {}", value));
}
这样编译错误就会显示:”Expected 5, got 4”,非常清晰!
目前需要跳一些圈(使用fmtlib的宏等),但等C++26正式发布时应该会有更简洁的方式。不过这个helper函数也不复杂,值得加到你的库里。
Ara分享了切换编译器的实战经验和踩坑指南
为什么要切换编译器?
现实情况:
切换编译器不是”flip a switch”那么简单,需要多个步骤,是一个过程。通常需要新老编译器并行运行一段时间,因为你不能告诉客户”半年内不更新了,我们在换编译器”。
基础设施准备:
依赖处理:
条件编译技巧:
#ifdefMY_PROJECT_GCC
#include<linux_headers>
#elifdefined(MY_PROJECT_MSVC)
#include<windows.h>
#else
#error"Unsupported compiler"
#endif
用项目特定的宏而不是通用的编译器宏,避免和第三方库冲突。用 #error强制处理新编译器的情况。
常见陷阱:
实现定义行为:
// char在ARM上是unsigned,Intel上是signed
charc=127;
while (c<128) { // ARM上死循环!
c++;
}
// size_t vs unsigned
autopos=name.find("--"); // 返回size_t
if (pos!=std::string::npos) { // 64位系统上pos被截断成32位
returnname.substr(pos); // 可能access violation
}
解决方法:用auto或正确的类型,用 {}初始化检测narrowing conversion。
策略建议:
新项目的话,从一开始就用多个编译器并行构建,避免绑定单一编译器的非标准特性。
Dimitra介绍如何用HPX进行SIMD编程
SIMD基础:
Single Instruction Multiple Data - 用一条指令处理多个数据。比如一次加8个double,而不是一个一个加,性能提升可达8倍。
C++中的SIMD:
C++26引入 std::simd<T>(C++26之前在experimental命名空间)
std::simd<int> a(5); // 所有lane都是5
std::simd<int> b([](int i) { returni; }); // 0,1,2,3...
autosum=reduce(a+b); // 水平求和
传统vectorization需要手写两个循环:
// vectorized部分
for (size_ti=0; i<n-n%simd_size; i+=simd_size) {
simd<float> v=load(data+i);
do_something(v);
}
// scalar tail
for (size_ti=n-n%simd_size; i<n; ++i) {
do_something(data[i]);
}
HPX的优势:
HPX自动处理vectorization和tail:
hpx::for_each(hpx::execution::simd,
data.begin(), data.end(),
do_something);
组合并行和SIMD:
hpx::for_each(hpx::execution::par_simd, // 并行+SIMD
data.begin(), data.end(),
do_something);
实战案例 - 图像模糊:
voidblur_image(constBMP& input, BMP& output) {
intheight=input.TellHeight();
std::vector<int> rows(height-2);
std::iota(rows.begin(), rows.end(), 1); // 1到height-2
hpx::for_each(hpx::execution::par_simd,
rows.begin(), rows.end(),
[&](int y) {
for (intx=1; x<width-1; ++x) {
autotop=input(x, y-1);
automid=input(x, y);
autobot=input(x, y+1);
autoblur= [](auto a, auto b, auto c) {
returnstd::clamp(0.25*a+0.5*b+0.25*c, 0, 255);
};
output(x, y).Red=blur(top.Red, mid.Red, bot.Red);
output(x, y).Green=blur(top.Green, mid.Green, bot.Green);
output(x, y).Blue=blur(top.Blue, mid.Blue, bot.Blue);
}
});
}
HPX让你专注业务逻辑,自动处理并行和vectorization的细节,性能优秀且代码简洁。
Joel分享Eve和Kiwaku库的设计经验
核心理念 - SOLID for Generic Programming:
传统SOLID是给OOP的,但泛型编程也能用:
Callable Objects设计:
Eve的所有函数都是callable object,支持options:
autoresult=eve::add[eve::saturated](a, b); // 饱和加法
autoresult=eve::cos[eve::pedantic](x); // 精确但慢
autoresult=eve::mul[eve::lower](x, y); // 快但不精确
实现骨架:
template<typenameOptions>
structabs_t : elementwise_callable<abs_t, Options, saturated> {
template<valueT>
Toperator()(T x) const {
EVE_DISPATCH_CALL(x);
}
EVE_CALLABLE_OBJECT(abs_t, abs);
};
inlineconstexprautoabs=functor<abs_t>;
编译时分发:
用if constexpr替代多个重载:
autoabs_impl(auto x) {
ifconstexpr (unsigned_value<T>) returnx; // 无符号直接返回
elseifconstexpr (floating<T>) return /* formula */;
elseifconstexpr (has_intrinsic<T>) return__builtin_abs(x);
elsereturn /* fallback */;
}
27个if constexpr分支,而不是27个函数重载!编译更快,错误信息更清晰。
错误处理技巧:
Toperator()(T x) const // 好 - 错误在这停住
// auto operator()(T x) const // 坏 - 错误继续传播
ifconstexpr (/* can call */) returnadapt_call(...);
elsereturnignore{}; // 可转换为任何类型,但触发static_assert
Static Assert vs Concept:
Concept用于API级别的类型检查,static_assert用于逻辑错误:
template<integralT, intN>
autoshift_right(T x) {
static_assert(N<sizeof(T)*8, "shift amount too large");
// 调用合法,但值不合理
}
编译性能:
旧版本(tag dispatch):8.5秒
新版本(concept + if constexpr):4.3秒
错误信息也短了一半
Kiwaku容器库:
用CTAD + named parameters构造:
autov=view{
of=data, // 数据源
shape= {2, 3, 3}, // 维度
label="my_data" // 标签
};
缺点:类型名超长(需要类型擦除支持)
小技巧:detail命名空间用 _而不是 detail,符号表更小,编译更快!
总结:
Roth Michaels分享如何让自己变得”过时”(实际上是关于领导力和知识传递)
核心思想:
不是让自己变得不可替代来保住工作,而是通过培养团队、传递知识,让团队不再需要你,从而解放自己去做更重要的事情。
Free Jazz哲学的领导方式:
Roth本身是音乐家出身,他从带乐队的经验中学到:不要直接告诉别人怎么做,而是通过示范和引导。就像free jazz的即兴演奏,有结构但允许自由发挥。
关键经验:当乐队成员没按你想法演奏时,不要说”你错了”,而是通过你自己的演奏来引导他们。Show, don’t tell.
培养领导者:
识别三类潜在领导者:
共享领导权:
和同事co-tech lead,一起做决策。创建Technology Strategy Forum而不是Architecture Team(后者听起来太ivory tower)。每个项目都找志愿者来lead,而不是自己包揽。
知识分享技巧:
1.公开回答问题 - 有人私信问问题,回答到公开频道,让所有人都能搜索到
2.别做第一个回答者 - 看到问题先等等,给别人机会。设置3小时后提醒,看看有没有人回答
3.提供参考资料 - 不只给答案,还要给talk链接、CPP reference链接,让人知道怎么学
4.苏格拉底式提问 - 别直接给答案,问”为什么这样做?”“如果这样会怎样?”
5.五个Why - 学习丰田制造业,问5次为什么找到问题根源
Pair编程/调试:
Code Review原则:
明确标注评论类型:
警惕”Looks Good To Me” - 可能是他们没看懂你的复杂代码,而不是代码真的对。
对于复杂PR(模板元编程、多线程),主动提供walkthrough教学。
避免成为”幽灵”:
最怕的情况:”因为某某说的”,但那人已经离职多年。团队要理解”为什么”,而不是盲目相信权威。
当有人说”因为Roth说的”,立刻纠正:”不,你要理解原因,我可能是错的。”
拥抱失败并公开:
展示自己的错误决策:”这个设计是我当时从Objective-C角度想的,但在C++里是错的。”
鼓励团队公开谈论失败:性能回归差点发布,团队不是偷偷修,而是大声宣传”我们犯错了,学到了教训,现在有了工具避免重犯”。
情感层面的放手:
你负责的产品是你的”孩子”,但现在别的团队接手了,他们的方向可能和你不一样。要学会放手,它们不再是你的了。
AI的可能性:
半开玩笑提到:能不能训练一个”Roth chatbot”,用所有Slack历史Q&A训练,让它回答问题?
总结:
真正的领导力不是让自己不可替代,而是:
这样你的团队成长了,不再需要你,你就可以去做下一个更重要的事情。