映射:运算符[] 应该为[[nodiscard]]
Map: Operator[] Should Be Nodiscard

原始链接: https://quuxplusone.github.io/blog/2025/12/18/nodiscard-operator-bracket/

Libc++ 正在其头文件中越来越多地应用 C++17 的 `[[nodiscard]]` 属性, 效仿 Microsoft STL。该属性会标记函数的使用,其中返回值被故意忽略,通常表示一个错误。虽然对于像 `malloc` 这样的函数很有用,但普遍应用存在争议。一些函数,例如 `unique_ptr::release`, 故意没有被标记,因为丢弃其返回值可能是故意的,尽管这种情况不常见。 最近一个涉及 `map::operator[]` 的案例突显了这个问题。最初 Libc++ 标记了 `[[nodiscard]]`,但在 Google 报告了合法的用法后,该标记被撤销,这些用法仅仅是为了其副作用(修改 map)而调用该函数。作者认为 `map[key]` 是 `map.try_emplace(key)` 的一个糟糕替代品,供应商不应该鼓励这种做法。 结论是,虽然 `[[nodiscard]]` 可以提高代码质量,但上下文很重要。使用 `m[k]` 习惯用法代码库应该重构为使用 `m.try_emplace(k)` 或显式转换为 `void`,以确认预期的副作用。

Hacker News 新闻 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 Map: Operator[] 应该被标记为 Nodiscard (quuxplusone.github.io) 8 分,由 jandeboevrie 发表于 2 小时前 | 隐藏 | 过去 | 收藏 | 讨论 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

Lately libc++ has been adding the C++17 [[nodiscard]] attribute aggressively to every header. (I’m not sure why this month, but my guess is that libc++ just dropped support for some old compiler such that all their supported compilers now permit the attribute even in C++11 mode.) libc++ is following the trail that Microsoft STL has blazed since VS 15.6 in late 2017.

Some functions, like malloc, always make sense to mark [[nodiscard]]. Others, like unique_ptr::release, are deliberately left unmarked because even though it is usually a bug to write

sometimes that’s actually what you meant — you’re calling it only for its side effect. Back in 2022, Stephan T. Lavavej estimated of unique_ptr::release that “90% of discards are a bug, but 10% are maybe valid… Even though it would find bugs, the cost in false positives is too great.” However, I think it’s worth pointing out that the fix for a false positive is trivial: all you have to do is refactor that line of code into

and the warning goes away. So personally I’d apply [[nodiscard]] even to unique_ptr::release. But it would clearly create noise to apply [[nodiscard]] to, for example, printf (which returns int) or vector::erase (which returns an iterator).


An interesting borderline case came up this week. In llvm-project#169971, Hristo Hristov marked libc++’s map::operator[] as nodiscard. The assumption was that it is usually a bug to write

Hans Wennborg quickly reported that actually Google’s codebases do this a fair bit. Statements calling map::operator[] purely for its side-effect are found in Chromium:

// Map the result id to the empty set.
combinator_ops_[extension->result_id()];

in V8:

// We need to insert the load into the truncation mapping as a key, because
// all loads need to be revisited during processing.
int32_truncated_loads_[op_idx];

and even in flatbuffers:

if (attributes.Add(kv->key()->str(), value)) {
  delete value;
  return false;
}
parser.known_attributes_[kv->key()->str()];

This last example doesn’t even come with a comment to explain it. If I owned this code, I’d absolutely worry that some coworker would come along and refactor it to

parser.known_attributes_[kv->key()->str()] = false;

But that would be incorrect! Assignment-of-false is an unconditional assignment; but what we have above is a conditional assignment: “If this key isn’t yet in the map, then assign it false; if it is already in the map, leave its value alone.” That is, it’s the verb that the STL calls .try_emplace, and as a maintainer I’d insist that that line be rewritten for clarity into

parser.known_attributes_.try_emplace(kv->key()->str(), false);

However, if that’s not possible (maybe because the codebase needs to keep working pre-C++17), then we can at least add the cast-to-void:

// Map to "false" only if the attribute isn't already mapped
(void)parser.known_attributes_[kv->key()->str()];

Actually, the GitHub history records that the code originally had a very clear (albeit slightly inefficient) if statement:

if (known_attributes_.find(attribute->key()->str()) == known_attributes_.end())
  known_attributes_[attribute->key()->str()] = false;

which was replaced with the current inscrutable version during review.


Anyway, libc++ responded by removing [[nodiscard]] from map::operator[] again in #172444. I think they should have stuck to their guns: mymap[key]; is a horrible way to write mymap.try_emplace(key) and STL vendors shouldn’t condone it.

Microsoft STL doesn’t mark map::operator[] as nodiscard, either, presumably because they encountered this “idiom” in the wild too. Microsoft (and now libc++) do mark the side-effect-less operator[]s of array, deque, and vector.

LLVM internally doesn’t mark the operator[]s of llvm::DenseMap, llvm::MapVector, or llvm::StringMap, although my experimentation shows that they could do so with only a handful of minor fixups (here, here, here, here), in many cases by using the more expressive and efficient tryEmplace they’ve already implemented and used as a building block for their operator[].

PSA: If your codebase contains any instances of the m[k]; “idiom,” please change them to m.try_emplace(k); or to (void)m[k]; with a code comment. Your code reviewers will thank you, and so will your library vendors!

See also:

联系我们 contact @ memedata.com