C++ 中文周刊 2026-01-24 第194期

周刊项目地址

公众号

imgsrc="https://wanghenshui.github.io/cppweeklynews/assets/code.png"alt=""width="30%"

点击「查看原文」跳转到 GitHub 上对应文件,链接就可以点击了

qq群 753792291 答疑在这里

RSS

欢迎投稿,推荐或自荐文章/软件/资源等,评论区留言

一月邮件 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2026/#mailing2026-01


资讯

标准委员会动态/ide/编译器信息放在这里

编译器信息最新动态推荐关注hellogcc公众号 本周更新 2025-01-08 第288期

性能周刊

文章

Building Your Own Efficient uint128 in C++

想要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库快多了,因为编译器能把一切都优化成直线指令。

C++26 Reflection 💚 QRangeModel

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:区块,可能需要逐个函数加注解。

Modern C++ use in Chromium

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::RefCountedbase::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_* - 禁!会显著增加代码体积

唯一例外: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。毕竟是几千万行代码的项目,牵一发动全身。

Understanding C++ Ownership System

从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,降低了一半复杂度。

Time in C++: C++20 Brought Us Time Zones

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:

基本用法:


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系列:用Active Accessibility定位焦点位置

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。需要:

  1. 理解多个API的交互(GetGUIThreadInfo + IAccessible)
  2. 处理各种返回值格式(VT_I4 vs VT_DISPATCH)
  3. 考虑并发(窗口可能在操作中被销毁)
  4. 应对各程序的实现差异(裁剪问题)

Raymond的风格是一步步发现问题、解释原因、提供解决方案,非常适合学习Windows API

视频

C++ Weekly - Ep 435 - C++26's User-Defined static_assert Messages

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函数也不复杂,值得加到你的库里。

Switching the Compiler - Ara Hacopian - NDC TechTown 2024

Ara分享了切换编译器的实战经验和踩坑指南

为什么要切换编译器?

现实情况:

切换编译器不是”flip a switch”那么简单,需要多个步骤,是一个过程。通常需要新老编译器并行运行一段时间,因为你不能告诉客户”半年内不更新了,我们在换编译器”。

基础设施准备:

  1. 开发环境设置 - 可能遇到license、政治、IT部门的阻力
  2. CI环境 - 更复杂,可能需要USB dongle的license,云环境配置等
  3. 文档化 - 写文档或者更好的是Docker化构建

依赖处理:

条件编译技巧:


#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。

策略建议:

  1. 提前做研究 - 确认可行性
  2. 制定计划 - 不是”下周就搞定”
  3. 准备代码库 - 封装依赖、修复warning、重构构建脚本
  4. 从单元测试开始 - 依赖最少,先修复核心问题
  5. 使用静态分析和sanitizer - 提前发现问题
  6. 全团队参与 - 不要一个人默默修bug半年

新项目的话,从一开始就用多个编译器并行构建,避免绑定单一编译器的非标准特性。

SIMD and Vectorization with HPX - Dimitra Karatza

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的细节,性能优秀且代码简洁。

Library Design from Ergonomics to Implementation - Joel Falcou - NDC TechTown 2024

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个函数重载!编译更快,错误信息更清晰。

错误处理技巧:

  1. 在调用点报错,不在库内部
  2. 减少auto返回类型 - 明确类型能提前截断错误链:

Toperator()(T x) const  // 好 - 错误在这停住

// auto operator()(T x) const  // 坏 - 错误继续传播

  1. ignore技巧处理未实现:

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,符号表更小,编译更快!

总结:

How to Become Obsolete - Roth Michaels - Meeting C++ 2024

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训练,让它回答问题?

总结:

真正的领导力不是让自己不可替代,而是:

这样你的团队成长了,不再需要你,你就可以去做下一个更重要的事情。

开源项目介绍


上一期

本期

下一期

看到这里或许你有建议或者疑问或者指出错误,请留言评论! 多谢! 你的评论非常重要!也可以帮忙点赞收藏转发!多谢支持! 觉得写的不错那就给点吧, 在线乞讨 微信转账