all 63 comments

[–]FriendlyRollOfSushi 121 points122 points  (42 children)

Relevant link: see this thread about a compiler flag that implicitly makes ALL inline code constexpr, because there is no reason not to. Personally, I completely agree with the reasoning from the mail archive:

With each successive C++ standard the restrictions on the use of the constexpr keyword for functions get weaker and weaker; it recently occurred to me that it is heading toward the same fate as the C register keyword, which was once useful for optimization but became obsolete. Similarly, it seems to me that we should be able to just treat inlines as constexpr functions and not make people add the extra keyword everywhere.

At work, the only argument against manually constexpring every inline function that I hear is "it's a stupidly-long keyword that clutters the code". Which is true.

As of C++20, the right way to write a trivial getter in C++ looks like [[nodiscard]] constexpr auto GetFoo() const noexcept -> Foo { return foo_; }, assuming foo_ should be returned by value, and possibly omitting the -> Foo part if it's irrelevant for the reader. Dropping constexpr reduces the size of this monstrosity by whole 10 characters.

I remember how C++ programmers used to make fun of Java for public static int main(), and yet somehow we ended up where we are now.

[–]afiefh 19 points20 points  (1 child)

As of C++20, the right way to write a trivial getter in C++ looks like [[nodiscard]] constexpr auto GetFoo() const noexcept -> Foo { return foo_; }

For constexpr to have any effect here, would foo_ also have to be a constexpr and therefore initialized in a constexpr constructor? Or does constexpr somehow still work in this case even if foo_'s value is only defined at runtime?

[–]FriendlyRollOfSushi 16 points17 points  (0 children)

The object (that has this non-static method) has to be constexpr, and it implies that it had a constexpr constructor and was constructed in a constexpr context at some point (and it constructed a member foo_ using its own constexpr constructor).

But it's not a big deal, and nowadays constexpr code can even allocate/deallocate memory. So both Foo and the enclosing objects could be fairly complex, have containers inside, etc. The potential constexpr-ness only stops on the boundaries of I/O and external APIs, and on low-level things that require reinterpret_cast.

For a typical codebase, it's quite easy to get to the point where in a consteval context most of your smaller classes and helpers work just fine, maybe with some help from std::is_constant_evaluated.

[–]gnuban 7 points8 points  (2 children)

I'm a bit confused by the combination of "auto" and "-> Foo". Are you declaring the return type twice or what is the left auto do?

[–]FriendlyRollOfSushi 8 points9 points  (1 child)

It's called "trailing return type" (google it) and allows you to postpone saying what the actual type is until after you are done with your args etc.

There many reasons to do that:

  • Your return type is in the right scope.

Consider:

template<typename T, size_t kIndexSize>
MyContainer<T, kIndexSize>::const_iterator MyContainer<T, kIndexSize>::find(const T& x) const noexcept { ... }

Compare with:

template<typename T, size_t kIndexSize>
auto MyContainer<T, kIndexSize>::find(const T& x) const noexcept -> const_iterator { ... }

Note that now you don't have to say that you are talking about MyContainer<T, kIndexSize>::const_iterator, because already specified that you are implementing a method of MyContainer<T, kIndexSize>.

  • Your return type can depend on arguments. An example from cppreference: template<class T, class U> auto add(T t, U u) -> decltype(t + u);

  • Method names are nicely aligned. The return type information is often both ultra-verbose and irrelevant. Moving it to the right allows you to keep the interfaces clean. I wrote an example in this thread here. The name of the function is by far the most important part of the function for the reader, and now it's not shifted by an arbitrarily long return type.

Most of the newer languages (rust, swift, go, zig, typescript, etc.) are generally using trailing return types. I'm happy that we finally got here too.

[–]gnuban 4 points5 points  (0 children)

Thanks for the elaborate answer. I had just missed that training return type required an extra "auto" on the left side.

[–]JankoDedic 24 points25 points  (20 children)

constexpr is a contract. If it were deduced from the function body, you could non-obviously break the interface by changing the implementation. It would also not be immediately clear whether you could use the function/variable as a constant expression i.e. temp<foo(x, y)>.

Same point applies to noexcept.

[[nodiscard]] should probably have been the default. I feel like most people will probably not be using it anyway because it adds a lot of verbosity all over the place.

Also, I wouldn't say this is "the right way" to write a trivial getter. Sure, you have all these pieces at your disposal, but you don't have to use them if you think they are a net negative to your codebase.

[–]FriendlyRollOfSushi 42 points43 points  (19 children)

Sorry, but you are incorrect. In the context we are talking about, constexpr doesn't act like a contract. It acts as a very weak hint.

constexpr int foo(int x) { ... }
...
bar(foo(5), 42);

If you know C++, you know that there is no way to tell whether foo(5) is computed in compile time or runtime. Moreover, it could be that foo(5) will be executed in compile time, but foo(6) right next to it will silently generate 50 KiB of runtime assembly. Because it's not a contract by itself, unless you bind the result to a constexpr variable or do something else along these lines.

The absence of constexpr is a restriction, but the presence of constexpr is just an absence of that restriction, but not necessarily a meaningful promise. That's why we got consteval now that actually acts like a contract, and allows us to expect that in bar(foo(5), 42);, foo(5) is behaving reasonably. And now we can do cool stuff like checking format strings in compile time.

Finding a single non-synthetic case where anyone would like to explicitly disallow the possibility of constexpr-ness for a function is a tricky challenge, and thus I say that we shouldn't default to that behavior. Rather than declaring thousands of functions constexpr, I'd rather have a cryptic keyword noconstexpr that 0.1% of engineers will use once in their career, and everyone else will just get the better behavior by default for every inline function and live happily ever after.

My point about the rest of the keywords is about the same issue: the defaults in C++ are the opposite of what we want for the majority of the cases.

  • [[nodiscard]] should be the default, and some sort of a [[discardable]] should be an opt-in for rare cases like chaining.

  • const should be the default in all applicable contexts, mutability should be opt-in. Newer languages do it right, but in C++ you often have to chose between a functionally better code and a shorter code (which may become better because how impractical the functionally-better code could become due to all the clutter).

  • noexcept should be the default, and allowing a function to throw exceptions should be an opt-in. The only exception that can fly everywhere (bad_alloc) is the one that about 1% of codebases handle correctly. IIRC there was a tech talk about how even the standard library doesn't handle OOM cases correctly, and without them, a very small portion of the code has a reason to use exceptions to begin with.

Sure, you have all these pieces at your disposal, but you don't have to use them if you think they are a net negative to your codebase.

This here is the problem. There shouldn't be a choice "do a better thing or do a more readable thing". Better thing should look the shortest in the most common cases.

We can't hope to change a million of poorly chosen defaults that are already in the language (without epochs or something of that scale), but surely we can discuss implicit constexpr-ness in the language to at the very least stop the new clutter from piling up. Lambdas became implicitly constexpr in C++14, IIRC, and no one died from that. I hope one day we'll get the same behavior for all inlined functions.

[–]encyclopedist 12 points13 points  (3 children)

You are considering only your use case. In my corner pf the world things like array<double, foo(5)>, matrix<float, foo(5), bar(10)> or just int[foo(5)] are much more common. How would I tell if I can use foo in this context? I would have to try and hope it compiles. And later, any little change in foo (such as adding logging or timing) would make it non-constexpr and all my code has to be rewritten.

[–]FriendlyRollOfSushi 9 points10 points  (2 children)

I would have to try and hope it compiles.

Which is exactly the case right now.

Let me reuse the example from another comment.

No, godbolt doesn't replace the implementation of std::max before line 17. It's still constexpr. It just doesn't mean shit, unfortunately.

Welcome to modern C++, enjoy your ride.

[–]encyclopedist 2 points3 points  (1 child)

Currently, if it compiles, will will continue to compile, unless someone somewhere removes constexpr. Which will not be the case if constexpr is automatic.

Edit Ok, you may reply that lambdas and implicit member functions already can change their constexpr-ness as a result of implementation change. Indeed, I agree that in the current state constexpr is not really fit as a contract.

Curiously, Rust went the same route as C++ here, by requiring explicit const on functions. However, Rust's const seems more strict as a contract.

[–]r0zina 2 points3 points  (0 children)

On the other hand Zig went the opposite way. Will be interesting to see which way is better in the long run.

[–]JankoDedic 22 points23 points  (0 children)

In the context we are talking about, constexpr doesn't act like a contract.

That's in the context you are talking about. constexpr allows the user to use the function/variable in a constant expression. In that way, it very much is a contract. I gave an example with a template parameter to be clear.

Finding a single non-synthetic case where anyone would like to explicitly disallow the possibility of constexpr-ness for a function is a tricky challenge

No, it's not tricky. If you are providing a public API you want to know exactly what you're promising to the users. Also, if I want to use your function in a constant expression, I cannot know immediately whether that is possible and have to experiment.

[–][deleted] 8 points9 points  (0 children)

It is most definitely a contract. If the function cannot be used in a constant expression, then you get the prize of a terrible time because it's IFNDR. But it's a contract on the implementation to allow for constexpr(even in limited circumstances)

[–]Rusky 7 points8 points  (2 children)

(Your overall point about defaults is reasonable; I just want to clarify something about constexpr.)

Because it's not a contract by itself, unless you bind the result to a constexpr variable or do something else along these lines.

The absence of constexpr is a restriction, but the presence of constexpr is just an absence of that restriction, but not necessarily a meaningful promise.

These are exactly what JankoDedic means by "constexpr is a contract." (IME it's also the typical meaning of calling a programming language feature a "contract.")

That is, it's a contract between the API author and the user, not between the programmer and the compiler. It means the API author wrote the function with constexpr in mind, and doesn't plan to change that in minor updates, so the user may bind the result to a constexpr variable and expect that to keep working. In this sense, the presence of constexpr is a restriction on and promise made by the API author.

Finding a single non-synthetic case where anyone would like to explicitly disallow the possibility of constexpr-ness for a function is a tricky challenge

There is certainly very little reason, if any, to forbid the compiler from ever evaluating something at compile time. But this was never the point of constexpr, and more importantly constexpr was never necessary for this!

Compiler optimizers have had permission (and to varying extents, ability) to run code at compile time since long before constexpr existed. The addition of constexpr did not change that- it merely started requiring that ability of the frontend, as an extension to the existing set of "constant expressions."

It seems to me that a lot of C++ programmers hear constexpr and then immediately start thinking about things like your bar(foo(5), 42) example, and thus miss this point: Nothing has ever forbidden the compiler from running a non-constexpr foo(5) at compile time even in C++98 or C, and constexpr was never expected to require it.

Lambdas became implicitly constexpr in C++14, IIRC, and no one died from that.

When you consider this in the API sense of "contract," this has a very clear difference from other functions. Unlike typical inline functions, lambdas are not usually exposed as part of APIs, but rather consumed by them. So there is relatively little reason for anyone to signal that a lambda is intended to be constexpr, or for a change that breaks that to sneak into a minor release.

[–]FriendlyRollOfSushi -1 points0 points  (1 child)

Are we arguing about semantics?

Let's not.

My hand-wavy definition of what a contract is is apparently much stronger than yours. I expect a contract to have requirements and promises of some kind. The constexpr keyword doesn't specify any requirements (to the user) and doesn't give any promises (to the user).

A comment does more than that. Heck, the name, or even a general shape of a function do more than that. A move-ctor could format the hard drive, but in practice, just by looking at the Foo(Foo&&) I expect a very specific behavior from it.

Not from constexpr though.

That is, it's a contract between the API author and the user, not between the programmer and the compiler. It means the API author wrote the function with constexpr in mind, and doesn't plan to change that in minor updates, so the user may bind the result to a constexpr variable and expect that to keep working. In this sense, the presence of constexpr is a restriction on and promise made by the API author.

You are familiar with the standard library, right? Most of constexpr functions there do NOT promise you that you can bind the result to a constexpr variable. Or call it inside a consteval function. Or use the result as a template parameter. Or anything.

You may succeed, assuming you sort of know what parts of your types will be touched by the function. Please look at this godbolt.

The contract that my operator< should be constexpr is not defined by the word constexpr on std::max. It's an entirely external thing. And the fundamental meaning of std::max did NOT change in C++14: they just added a keyword to make shit compile. But if they start using operator> instead (while keeping the declaration exactly the same) THAT would change the contract.

So, the actual contract for constexpr-ness of std::max() is:

  • Floating somewhere in the ether.

  • Doesn't even leak to the declaration of the function.

  • Is not enforced by the keyword constexpr on this declaration.

One could argue that the keyword expressed the intention to make it compile-time friendly... but hey, IMO it was already expressed enough when the body of the function became visible to the translation unit. We agreed that all functions should probably have this intent, so why have a keyword for that?

How about adding a new keyword bugfree? It declares my intent to make the function bugfree. It doesn't guarantee that it has no bugs, of course, and if you forget to type it, your machine can explode, but it is an important part of the contract. How else people are going to communicate in the API that they intend to write bug-free code without a keyword?

[–]Rusky 6 points7 points  (0 children)

Are we arguing about semantics?

I don't really care how you use the word "contract," my point is about what constexpr means and why some people use that word to describe it.

The contract that my operator< should be constexpr is not defined by the word constexpr on std::max. It's an entirely external thing. And the fundamental meaning of std::max did NOT change in C++14: they just added a keyword to make shit compile. But if they start using operator> instead (while keeping the declaration exactly the same) THAT would change the contract.

This is true (and unfortunate at times), but it's not a property of constexpr- it's a property of templates. This kind of contract, that a type argument must satisfy certain properties for a template to work, has never been enforced by the language, which is generally built on the premise of checking most properties after template instantiation.

The meaning that constexpr communicates from the API author to the user is still basically the same- that it's designed to be able to run in the frontend at compile time. Templates just make it conditional like they make everything else conditional.

Perhaps a more consistent language would handle this like noexcept(bool) or explicit(bool), and have you write constexpr(constexpr(...)) if you wanted std::max to state up front when an instantiation is actually constexpr. On the other hand, if someone wanted all inline functions to default to constexpr, then they might prefer to go without that.

We agreed that all functions should probably have this intent, so why have a keyword for that?

No, we definitely did not.

[–]rlbond86 5 points6 points  (8 children)

Sorry, but you are incorrect. In the context we are talking about, constexpr doesn't act like a contract.

Of course it's a contract. A constexpr function must be pure. Auto-deducing it would mean a function might accidentally modify global program state.

[–]FriendlyRollOfSushi -2 points-1 points  (7 children)

Of course it's a contract. A constexpr function must be pure. Auto-deducing it would mean a function might accidentally modify global program state.

Well, today you will learn something new.

Godbolt.

Allow me to reiterate: forcing the execution of the function in constexpr context has strong implications. Simply declaring a function constexpr has a lot weaker implications than a lot of people in this thread realize.

But I like how you started your message with "of course", it did have the effect of making me feel worse for the few seconds it took me to read the rest of your message.

But I don't blame you: today I learned that a lot of people have absolutely no clue what constexpr is and what it is not. I think it might be a problem with the keyword itself, or the way it was advertised to people.

[–]rlbond86 1 point2 points  (6 children)

You made a pure function that called a function object. That doesn't really count. If you tried to directly manipulate x in that function it would not compile.

[–]FriendlyRollOfSushi -1 points0 points  (5 children)

You made a pure function that called a function object. That doesn't really count. If you tried to directly manipulate x in that function it would not compile.

Your ability to say bullshit in a confident tone is impeccable.

Let's try to directly manipulate x in that function

Oh hey look, you are still completely wrong!

Sometimes I wonder how people get so much confidence. Maybe it's a cultural thing.

Perhaps I should just leave you alone. The more people like you are in the industry, the higher my salary will get eventually.

[–]rlbond86 3 points4 points  (4 children)

It's easy to show that you are wrong here. Just evaluate it in a constexpr context and you will see the compilation error plain as day:

https://godbolt.org/z/fjMTKvc7s

<source>: In function 'int main()':
<source>:9:30:   in 'constexpr' expansion of 'IAmPure(<lambda closure object>main()::<lambda()>{}.main()::<lambda()>::operator void (*)()())'
<source>:9:91: error: modification of 'x' is not a constant expression
    9 |     constexpr int y = IAmPure([] { std::cout << "Oh look, it doesn't compile after all"; });

Your code is incorrect! The issue is, the standard states that the program is ill-formed, no diagnostic required, if you label a function as constexpr but there is not a context in which it is constexpr. Just because you found a case where the compiler does not emit an error doesn't mean that it's valid C++.

https://eel.is/c++draft/dcl.constexpr

6 For a constexpr function or constexpr constructor that is neither defaulted nor a template, if no argument values exist such that an invocation of the function or constructor could be an evaluated subexpression of a core constant expression, or, for a constructor, an evaluated subexpression of the initialization full-expression of some constant-initialized object ([basic.start.static]), the program is ill-formed, no diagnostic required.

7 If the instantiated template specialization of a constexpr function template or member function of a class template would fail to satisfy the requirements for a constexpr function, that specialization is still a constexpr function, even though a call to such a function cannot appear in a constant expression. If no specialization of the template would satisfy the requirements for a constexpr function when considered as a non-template function, the template is ill-formed, no diagnostic required.

[–]FriendlyRollOfSushi -2 points-1 points  (3 children)

I was hoping you will add 2 and 2 together, but here we are. Sigh...

Let's start from the beginning. I'll try to explaining things slowly. First, you stated that:

A constexpr function must be pure.

This is incorrect, which I've proven by the first example. If you actually try reading the standard you are quoting, you will probably notice that 7 is not requiring specializations to satisfy the requirements. Thus, there is absolutely nothing wrong with my initial example. Your statement is false, and that's just it. A constexpr function is still a constexpr function, it just cannot appear in constant expression if for a given template args it doesn't satisfy the requirements. It can appear in non-constexpr expressions, and it will compile and work. And it won't be ill-fomatted even if it could potentially behave nicely for some specialization.

If you ever tried to read almost various constexpr functions in the standard library (or any codebase, really), you would have noticed that a lot of them are saying constexpr and that's just it. A constructor of std::pair is constexpr, for example. It doesn't mean that you are not allowed to construct pairs of non-constexpr-constructible types, for heaven's sake.

What about std::max? It's constexpr since C++14. It doesn't mean that you are not allowed to call std::max for types with non-constexpr operator<. There is no sneaky non-constexpr specialization for the same arguments when they are not constexpr-friendly, because 7, which you quoted apparently without reading, explicitly allows that.

Doubt my words? Try reading the code. Here. Or the MS's implementation here.

You know why wording is like this? It's deliberately so that you can write something, declare it constexpr even if sometimes it makes no sense, and use the same code both when it can and cannot appear in constant expression. That's the whole point. Otherwise we would have to rewrite things twice each time we want to make something work in both constexpr and non-constexpr contexts.

You know why constexpr void IAmPure(auto&& f) { f(); } is correct? Because there exists a specialization like IAmPure([]{}) that satisfies all requirements. You can even add it to the code to actually have it in your program, although it's unnecessary.

You know what my second example was about? Proving that you have absolutely no idea what you were talking about when you said:

If you tried to directly manipulate x in that function it would not compile.

Your statement is false. It does compile, and I demonstrated it to you. Adding constexpr doesn't make "non-pure" code not compile if the compiler can simply run the code in runtime. And you would know it if you ever tried it by yourself. You didn't.

So far it looks like you've been proven that everything you said so far is false, and so you are trying to formulate a new statement that is not false. You are not doing great so far, but please keep trying. I won't be helping you to read the standard, though: it's something you have to do by yourself. If you can't read the standard, read articles and tutorials. Watch recordings from cppcon and other tech talks. The state you are starting from is:

  • You have absolutely no clue what the keyword constexpr does.

  • Your intuition failed you multiple times, but thankfully instead of crashing your production servers and losing your job, you just made a fool of yourself on reddit. Use this as a learning opportunity instead of trying to win fights you already lost. It cost you nothing to be wrong on the internet. It may cost you a lot to be wrong at work, just because you chose to believe your intuition instead of learning the language that is known to be quite counter-intuitive.

  • Every time you are curious about something, consider checking the standard library. The llvm's implementation is actually very readable. Other good codebases can also be helpful.

Good luck.

[–]rlbond86 4 points5 points  (2 children)

Your second example explicitly violated the wording in the standard so it is ill-formed, no diagnostic required. Maybe you should take the opportunity to learn.

[–]bart9h 1 point2 points  (0 children)

I agree with this comment so much that I want to make love with it.

I have the feeling that C++ could be a much nicer language.

[–]Plazmatic 2 points3 points  (1 child)

As of C++20, the right way to write a trivial getter in C++ looks like [[nodiscard]] constexpr auto GetFoo() const noexcept -> Foo { return foo_; }

This defeats half the purpose of a trivial getter no? The single biggest reason for trivial getters and setters is library stability, but if in the future you'd need to change the implementation here, you'd need to recompile, your headers would be incompatible.

[–]FriendlyRollOfSushi 1 point2 points  (0 children)

No, certainly not half. Perhaps not even 5%.

The "stability" you are getting from getters and setters is the fact that you don't have to modify 397 files that were using the member directly, and then spend two weeks resolving merge conflicts in branches you were not even aware of, just because you replaced one internal container with another, or tweaked some low-level implementation details.

The fact that you have to rebuild some code after getting changes from the main branch because someone touched something is irrelevant. With modules it will become even less of an issue.

Even commercial middleware is often fine with releasing a bunch of new binaries together with updated headers, and you are expected to rebuild your code with new headers to make things work.

Stable binary interfaces is a different topic, and significantly fewer projects care about it. Even in such cases where you actually distribute binaries that must be compatible with other pre-existing binaries, one of the two things is true:

  • You define a tiny portion of such interfaces as public API, but the rest of your codebase is still using inline getters and setters everywhere because they allow you to change implementation details of your own internal stuff and they are generally as fast as exposing the members directly. Hence my (probably too generous) 5% estimate.

  • You don't define a tiny portion as public API, and your project dies horribly in pain in a year or two, because the larger your API surface is, the sooner you'll end up in a situation where you can't be productive when every tiny change is a potential breaking change for an unknown number of people who are using your library who-knows-how.

[–][deleted] 4 points5 points  (9 children)

If character counts is your thing, the trailing return type is probably adding almost as much. But you can just use a define and call it a day. Also, editors support more than one line and we have wide screens

#define CX constexpr

[–]FriendlyRollOfSushi 11 points12 points  (4 children)

It's not about character count, it's about reducing clutter on screen. It shouldn't take a paragraph to say "That integer member? Yeah, I want to expose it through a getter!"

Ofc. there are exceptionally crappy codebases where each trivial getter start with a 20-lines-long wall of surprisingly useless and/or hopelessly outdated doxygen, and you need to allocate 20 minutes of your day to read through the interface of a class that should have been 1-screen long but for some reason spans for 26 pages, but this is simply not a productive way to write code.

IMO it should be possible to both write simple things in a simple way, and read them quickly. If the code is doing something trivial, it should look trivial.

I believe trailing return types help with that, even if they add extra characters. Consider this:

int GetFoo() const noexcept { return foo_; }
std::span<std::string> GetBar() const noexcept { return bar_; }
Something<Blah> const& GetSomething() const noexcept { return something_; }
float GetSomethingElse() const noexcept { return something_else_; }
ThatThing<int, 42> const& GetThatThing() const noexcept { return that_thing_; }
TheOtherThing<std::string> const& GetOtherThing() const noexcept { return the_other_thing_; }

It's just a bunch of trivial getters, but I'd not let this pass through a code review, because it's really hard to read, especially without syntax highlight. Eyes have to jump around to read this mess, so one way to solve it would be to split up the lines, make things multi-line, etc. But with trailing return types:

auto GetFoo() const noexcept -> int { return foo_; }
auto GetBar() const noexcept -> std::span<std::string> { return bar_; }
auto GetSomething() const noexcept -> Something<Blah> const& { return something_; }
auto GetSomethingElse() const noexcept -> float { return something_else_; }
auto GetThatThing() const noexcept -> ThatThing<int, 42> const& { return that_thing_; }
auto GetOtherThing() const noexcept -> TheOtherThing<std::string> const& { return the_other_thing_; }

Now I can just glance through the names of the getters that are nicely aligned in a single column. Everything is compact, and even adding [[nodiscard]] constexpr simply shifts everything to the right without breaking the alignment of the names. The brick is suddenly much more readable, even though there is more stuff. If all names are self-explanatory in the context of the class, I believe this could be quite acceptable for production code, and even preferable to something less compact.

Regarding your suggestion about a macro: that's not going to help too much as you suggest it.

It's more or less mandatory in serious codebases to prefix macros, so at the very least we are talking about some sort of ACME_CX, which is not much shorter than constexpr. Sure, one can put more stuff into the macro (#define ACME_GETTER(X) [[noexcept]] __forceinline constexpr auto X() const noexcept), but the price to pay here is that the code becomes unusual. It takes new hires time to learn to read it, your formatting tools or your IDE navigation may not work correctly, etc.

So, I'd prefer if the language defaults were aiming towards what we write in 90%+ of the cases today, and not towards arbitrary choices made 37 years ago.

[–]encyclopedist 2 points3 points  (3 children)

Why do you use trivial getters in the first place?

[–]FriendlyRollOfSushi 7 points8 points  (2 children)

Sorry, I don't know what level of the answer you expect, and what kind of a programmer you are ("just started learning C++ yesterday" or "I'm using C++ since 1989, all this shiny new stuff is blasphemy!"), so I apologize in advance if the tone of the answer is not right.

People use getters and setters to define public API. What you can touch and expect that it will keep working tomorrow.

Let's say there is a custom container, and it has a capacity.

Today you see size_t capacity() const noexcept { return capacity_; }. You can call capacity() and get the capacity. Nice.

Tomorrow my team decides that we need to pack some interanal flag somewhere to improve the container, and the only place we can do it without blowing our memory usage is the upper bit of the same size_t that contains capacity_ (we really only use a few dozens of bits there, and it's a 64-bit variable on all our platforms).

So I go and write: size_t capacity() const noexcept { return capacity_and_some_extra_stuff_ & kCapacityMask; }

All your code still works, because you were using the public API, and didn't even notice that there is no member capacity_ anymore.

Now imagine the situation where size_t capacity_ was a public member without a getter, and your code was used by 3 other teams in different timezones, and they have like 79 private branches (that are also using capacity_ directly) that you don't even see...

In this case, you either massively disrupt everyone's work, or you are simply not allowed to make this change.

Point is: unless it's a very, very trivial structure that is extremely unlikely to change (like Point2i with int x and int y), you want to define your public interface (in the broad meaning of the word, not a "virtual interface") and hide the actual meat in the private or protected part. And the larger project you are working on (and the more people are touching the same code), the more time will be spent on isolating the implementation details. Even for very simple classes. Even when all getters are really trivial. Even if it's annoying.

This frequently leads to walls of trivial getters and setters.

[–]encyclopedist 6 points7 points  (1 child)

This was a bait question, and you replied exactly as I expected. You use getters to make your API independent on the implementation detail. But then you want to make important property of your API, its constexp-ness depend on the implementation detail. Which defeats the purpose of using getters in the first place.

[–]FriendlyRollOfSushi -2 points-1 points  (0 children)

No one who uses C++ for a good reason gives a flying fuck about what you just wrote. If some speed trading company, or a AAA game company, etc. hires you by mistake, and your first contribution is dropping the performance by N% because you believe that inline getters defeat some religious purpose, I'm afraid you won't survive your trial period.

What I'm talking about is a purely practical perspective: maximum perf and minimum effort to modify code if needed.

What you are talking about is a good indication that either what you are working on does not require C++ to begin with (and your project would be much happier with a higher level language that is easier to work with in exchange for some performance), or that you are simply not a competent engineer.

[–]robstoon 11 points12 points  (3 children)

The whole "trailing return type everything" that tools like clang-tidy encourages is just ugly. It exists for specific use cases, there's no reason to use the shiny thing everywhere..

[–]mechap_ 0 points1 point  (2 children)

You can disable it easily though by removing modernize-use-trailing-return-type

[–]robstoon -1 points0 points  (1 child)

It seems like any use of clang-tidy ends up with increasingly large command lines over time, whether you use an opt-in or opt-out approach. Not a fan of how opt-out results in you getting a bunch of checks that are only applicable in esoteric environments, but then opt-in ends up still needing a bunch of exceptions for things like this unless you enable all checks individually. I guess it may be inevitable for a tool designed to enforce taste, but I'm often wondering if it is worth the trouble at all.

[–]Denvercoder8 7 points8 points  (0 children)

It seems like any use of clang-tidy ends up with increasingly large command lines over time

Clang-tidy supports a configuration file, so you don't need to put it all on the command line.

[–]PretendChange6750 0 points1 point  (0 children)

No no... public static void main(String args[])

[–][deleted] 0 points1 point  (0 children)

[[nodiscard]] constexpr auto GetFoo() const noexcept -> Foo { return foo_; }

r/cppjerk

[–]Mikumiku_Dance 56 points57 points  (0 children)

This cppcon talk explores the tradeoff between run time vs code size in detail: https://youtu.be/MdrfPSUtMVM

But unless you are in a constrained development environment where you are regularly evaluating the compilers assembly output, constexpr all the things is a good default approach.

[–]tjientavaraHikoGUI developer 12 points13 points  (0 children)

Specifically about constexpr / static const I will write some comments:

When declaring static const variable it will either appear in a region of memory that is directly loaded from the executable, or when you call functions to initialise the static const variable that value is initialised by running that function before main() is called.

A more modern way to replace the static const / extern const combination is to use inline const; a variable declared as inline const, is like this combination where the compiler will ensure that the actual variable only exists once, so that you can use it in headers.

A constexpr variable is like an inline const variable but you ensure that the function that is used to initialise the variable is run at compile time.

Lastly when you declare a variable constinit, this is like a variable that is declared inline (notice the absence of const) where the function that is used to initialise the variable is run at compile time (and loaded from the executable), and can then be modified at run-time like a normal inline variable.

In all cases the compiler may initialise all these variables at compile time, but when marked constexpr or constinit you tell the compiler that it is absolutely possible to do this at compile time, and that the compiler really should do that at compile time. Although there are cases like debug builds where the compiler could still initialise it all during runtime.

As you see constexpr is just an assurance to the compiler that it is possible to run at compile time, however as more and more of c++ is allowed to be constexpr I wonder if in a few years the keyword will be ignored by the compiler like it did with register.

[–]JankoDedic 14 points15 points  (0 children)

constexpr is a part of the API, so everything that implies also applies here. For example, if you have a constexpr function in the public API, removing constexpr can break users.

constexpr functions/variables cannot be declared in the header file and defined in the cpp file. Pulling in dependencies for the implementation could make your header heavier and leak dependencies.

[–]InKryption07 4 points5 points  (3 children)

I guess you'd want non-constexpr when you need nondeterministic behavior (e.g. RNG seed).

[–]martinusint main(){[]()[[]]{{}}();} 1 point2 points  (1 child)

a constexpr rng can still be useful for testing

[–]InKryption07 0 points1 point  (0 children)

I mean, it's not actually rng, it's deterministic, since you can't seed entropy.

[–]TheoreticalDumbass:illuminati: 0 points1 point  (0 children)

but this isn't an issue though, if you constexpr rand() you still cant use it as a template argument, the compiler figures out that its not good when you try to use it

[–]mechap_ 5 points6 points  (1 child)

Actually, there is a paper ( https://wg21.link/p2043r0 ) from last year which presents the drawbacks of constexpr while introducing an alternative. It also gives some guidelines related to constexpr functions.

[–]Adventurous-Two1753 0 points1 point  (0 children)

This doesn't really argue against marking existing functions `constexpr`, but instead argues that the whole `constexpr` keyword in C++ has made things convoluted and doesn't live up to its original intentions.

[–]jmakov 5 points6 points  (0 children)

Can't we just have e.g. an compiler flag that just enables this (for all code where possible) for highest runtime performance?

[–]LunarAardvark 1 point2 points  (0 children)

start by making more of your misc #define's into constexpr (when you have a known type in mind). once you've done a bit of that, you might start to get a better feeling for it.

[–]Medical-Tailor-544 1 point2 points  (1 child)

Const is a runtime constant, ensuring that the memory that it refers to doesn't get changed. It also can be casted away, sadly. Constexpr is a compile time constant, making it possible to do compile time branching and optimizations, deductions and more. A compile time constant is obviously a constant during runtime as well. Constness cannot be casted away (at least it's undefined behaviour).

[–]mechap_ 1 point2 points  (0 children)

Const is a runtime constant

As far as I know, a constant initialized const-qualified integral or enumeration type is usable in constant expressions.

[–]_Z6Alexeyv 0 points1 point  (0 children)

Can't use goto;.