c++constantsconstexprnoexceptcpp-core-guidelines

Why should I be careful in "noexcept-ing all the functions" just like I "const all the things"?


I haven't found a video about const all the things ¹, but there's at least Con.3: By default, pass pointers and references to consts from the Cpp Core Guidelines.

There's lots of presentations and other resources suggesting to constexpr all the things, e.g. CppCon 2017: Ben Deane & Jason Turner “constexpr ALL the Things!”², but that, as suggested in the comments, doesn't really parallel either of const or noexcept, because it does not alter the interface of a function, but just adds extra functionality.

But as far noexcept goes, I don't seem to find as much "enthusiasm". The suggestions are more often skewed towards being very careful with it:

But isn't all that true for const as well? When I declare a function parameter as const&, am I not making a decision about the interface that I will not be able to revert in the future because it would break all my clients?

Though, I feel like putting const all over the place passes the test of time more easily. Take the case of a function that was designed in the early days of C++11 to loop on standard container with read access. I suppose³ one could have designed it like this

void work(auto const& r) {
    for (auto const& e : r) {
        …
    }
}

Why should work have write access to r at all? auto const& r seemed the right thing to do, I maintain, at least in "ordinary" scenarii.

But with C++20 (or even C++14 + Range-v3), that's not true anymore, not even in "ordinary" scenarii, because one should take into account that callers might not pass containers to work on, but views of some sort. Crucially, those views might require to mutate themselves, when looped over, as is the case for filter_view:

std::vector<int> v;
work(v);                                          // ok
work(v | std::views::transform(std::identity{})); // ok
work(v | std::views::filter(std::identity{}));    // compile-time failure

The last line doesn't compile with the provided declaration of work.

The solution, at least in this case, is that work should accept auto&& instead of auto const&.

But doing such a change doesn't break the two lines which were // ok in the first place, so it turns out that the ancient decision of declaring work's r parameter as auto const& was not really a blood oath as much as putting noexcept seems to be according to common lore.


(¹) But I kinda remember the title constexpr all the things was a "spin-off" of an earlier const all the things title from somewhere; I kind remember having heard Kate Gregory say the latter in some video; or maybe I just misremember, but I definitely think that it's a general suggestion; yes, with the due caveats, sure.

(²) With due caveats, and maybe there's some opposite opinion too, such as in Don't constexpr All the Things - David Sankel [CppNow 2021], which I haven't watched yet, but I maintain there's way more instances of do constexpr all the things around.

(³) I've started my career in 2019, so I've never been a pre-C++17 programmer, so I'm making assumptions about the past that could be wrong.


Solution

  • But isn't all that true for const as well? When I declare a function parameter as const&, am I not making a decision about the interface that I will not be able to revert in the future because it would break all my clients?

    Yes.

    One needs to actually put effort into designing good interfaces. Try to approach it as: could an implementation reasonably need this power?

    So if throwing is impossible because there's no way for the function's operation, not the current implementation, to fail or because you have another way to communicate the only types of failures... use noexcept.

    Likewise, if there's a possibility a function may need to return information and there's no other channel to do so... Use a pointer or reference to non-const. But the guidelines also favor using the actual return type to return information, over out or in-out parameters, so you shouldn't find yourself in this situation as often. Almost to the point that if you think you could treat a parameter as in-out, you probably already are going so (and modifying it).

    EDIT: That's how you can approach the decision. You also ask why you might feel differently about your decisions in the future in the two cases.

    Suppose you were too conservative and believed you didn't need to modify a parameter, but now you do. You change the parameter to take by reference instead of reference to const. How is the rest of your system affected? If the caller already had a modifiable value, it can pass it. If it didn't, compilation fails. This is the safest failure mode when changing code.

    Now, take noexcept. Suppose you were too conservative and added noexcept because you believed a function did not need to throw. Now, you want to throw so you remove noexcept. How is the rest of the system affected? It compiles. No errors. If called from another noexcept function, previously it was perfectly safe. Now, it still compiles, but it sometimes crashes (via std::terminate). You added a bug.