Whenever a library clause uses CTAD or list-initialization, it’s wrong

Over the past few years, I’ve noticed a pattern in several LWG issues: In library clauses, CTAD and list-initialization are never helpful and are sometimes harmful, in the sense that they create LWG issues to triage and fix. I’d noticed for several years “this happens a lot,” but I’d never collected all my anecdotes into one list. I’m starting that list now. As Wikipedia says, “This list is incomplete”; please let me know if you see any I’ve missed.

The CTAD offender is almost always the copy deduction candidate; see “Beware CTAD on reverse_iterator (2022-08-02) for details. CTAD on a multi-argument constructor can pick the wrong candidate sometimes too, but using CTAD to call a one-argument constructor is invariably a bug.

  • P0896R3: Copy deduction candidate. In R3, reverse_view::begin() was specified to return reverse_iterator{ranges::end(base_)}. Fixed in R4 by returning make_reverse_iterator(ranges::end(base_)) (which incidentally eliminated the list-initialization too).

  • LWG 3474: Copy deduction candidate. views::join(E) was defined as equivalent to join_view{E}. This did the wrong thing when E itself was an instance of join_view. Fixed by getting rid of the CTAD (but not, yet, eliminating the list-initialization).

  • LWG 4054: Copy deduction candidate. views::repeat(E) was defined as equivalent to repeat_view(E). This did the wrong thing when E itself was an instance of repeat_view. Fixed by getting rid of the CTAD.

  • LWG 4096: Copy deduction candidate. views::iota(E) was defined as equivalent to iota_view(E). This did the wrong thing when E itself was an instance of iota_view. Fixed by getting rid of the CTAD.

  • LWG 4172: Avoiding list-initialization is the house style (and rightly so). unique_lock{std::move(u)}.swap(*this) was editorially amended into unique_lock(std::move(u)).swap(*this) before application. (Notice that this isn’t CTAD; it’s a use of the injected-class-name](https://en.cppreference.com/w/cpp/language/injected-class-name.html).)

  • LWG 4293: List-initialization. span<T>::first was specified to return R{data(), Count}. This did the wrong thing (since C++26) when T was const bool, because then both const bool* and size_t would be convertible to bool, and span<const bool>{data(), Count} would be a span over an temporary array. Fixed by returning R(data(), Count).

Posted 2025-08-22