Skip to content

Const by default #815

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
MatthieuMv opened this issue Nov 12, 2023 · 16 comments
Open

Const by default #815

MatthieuMv opened this issue Nov 12, 2023 · 16 comments

Comments

@MatthieuMv
Copy link

MatthieuMv commented Nov 12, 2023

I believe that knowing which variable is subject to change easily reduce the mental overhead of code debug/reviewing/maintenance.
The issue with C++ is that you can declare a variable that should not change by forgetting the 'const' keyword.
C++ compilers throw an error when modifying a const variable but they can't tell you when you are modifying a variable that you shouldn't because of algorithm logic.

Adding 'const' to any variable and function argument is a practice that my team and I have been practicing over 3 years. The obvious issue with this practice is the increased mental overhead of writing code, as you have to remember it when writing a variable.

So, what do you think about initializing variable and function arguments with const by default ?
We can use a 'mutable' or 'mut' keyword to tell that the variable is subject to change when it is really required.

@JohelEGP
Copy link
Contributor

and function arguments with const by default

This is already the case for function arguments, which default to pass-by in.

@JohelEGP
Copy link
Contributor

@JohelEGP
Copy link
Contributor

Statements can also have parameters (https://github.com/hsutter/cppfront/wiki/Design-note%3A-Defaults-are-one-way-to-say-the-same-thing#from-named-functions-to-lambdas-to-parameterized-blocksstatements-to-ordinary-blocksstatements, #386 (comment)).
So if your const variable would be scoped to a statement,
you can use that to declare it:

(i := …) // `i` will be an `in` parameter, thus `const`.
  if (…) {
    …
  }

@ntrel
Copy link
Contributor

ntrel commented Nov 24, 2023

Declaring a runtime constant is quite noisy:

v := expr;
c: const _ = expr;

That's enough typing and noise to avoid marking constants. So the status quo encourages bad practice:
https://www.reddit.com/r/cpp2/comments/16ftsw7/suggestion_local_objects_const_by_default/

From https://github.com/hsutter/cppfront/wiki/Design-note:-const-objects-by-default:

the majority of local variables are, well, variables

I believe this statement is false in modern code. The author of the Vale language wrote:

To our great surprise, we've found that our codebases have a lot more declarations than assignments

We sampled three Vale projects. One had 111 declarations, and only 35 assignments. That's only 21% assignments! The other two were even lower, at 20% and 6%.

This isn't just Vale either. A randomly chosen Rust library, Rocket, had about 8%.

https://verdagon.dev/blog/on-removing-let-let-mut#why-we-like-it

@SebastianTroy
Copy link

SebastianTroy commented Nov 24, 2023 via email

@hsutter
Copy link
Owner

hsutter commented Nov 25, 2023

We sampled three Vale projects. One had 111 declarations, and only 35 assignments. That's only 21% assignments! The other two were even lower, at 20% and 6%.

Thanks, data is great and I appreciate it. Followup questions please:

  1. Do you know why they only counted assignments? Assignments are only one of the mutating operations.

  2. The three example reasons they give for fewer assignment seem to follow the pattern that writing in a more functional programming style (if-conditions, calling algorithms instead of loops) leads to less mutation, which is of course true -- the endpoint of that is the pure functional languages that have no mutation at all, only immutable state. I wonder how representative it is though of code in the major systems languages. In general, functional programming styles have not succeeded in getting broad adoption in the mainstream.

  3. Having only ~100 declarations seems like a very small sample.

Do you know of data on larger projects, ideally C/C++/C#/Java code, and counting the other non-const operations in addition to assignment?

@AbhinavK00
Copy link

AbhinavK00 commented Nov 25, 2023

I don't have the data but here's an article by creator of Odin language which talks about various kinds of syntax for declarations.

cpp2 seems to fall in the group of "name-focused" declarations and I can't help but notice that the two languages (Odin and Jai) which have name-focused declarations have same syntax for function, type and constant declarations. That is expected as functions and types are kind of "constant" bindings, you can't assign a new function to same name (not true for anonymous function but well, they are anonymous). Even Zig declares struct like the following

const A = struct {
    x: f32,
    y: f32,
};

cpp2 has an inconsistency in this case.

@JohelEGP
Copy link
Contributor

JohelEGP commented Nov 25, 2023

type implies const in t: @struct type = { }.
Much like Cpp2 v :== 0; and Cpp1 constexpr int v = 0; imply const for v.

@AbhinavK00
Copy link

That's not very obvious (I didn't even think about it like that before). I also don't know how that extends to functions and the constexpr examples doesn't really make sense to me since there are no mutable constant expressions and one can argue that it's a cpp1 thing too.

@JohelEGP
Copy link
Contributor

I also don't know how that extends to functions

Named functions are also constant.

@AbhinavK00
Copy link

Named functions are also constant, type also implies constant, why can't variables be constant too?

@JohelEGP
Copy link
Contributor

They can be constant.
It's just not implied by default.
You need to explicitly declare them const.

@hsutter
Copy link
Owner

hsutter commented Nov 25, 2023

I tried an experiment where every local variable is emitted as const by default, just to see what would happen in the cppfront code and test cases. Here are some initial observations, in no particular order.

Of 98 test cases, 41 succeeded and 57 failed. It seemed that the 41 were mostly the smaller/simpler ones that were exercising the compiler (e.g., testing grammar or lowering) rather than trying to do a sample computation.

In some cases, making a local variable const by default caused copies instead of moves to happen. This happened for return values and for forwarding parameters.

Some types' natural usage is non-const, including when they appear as local variables:

  • Local variables of dynamic type, such as std::variant and std::any, tend to be set after construction.
  • Local variables of container type, such as std::vector and std::map, tend to be modified to accumulate results. Typical patterns included push/emplace/pop, and modifying the container's contained data via an iterator or via a mutating range-for loops.
  • Local std::string objects tend to be modified. Typical patterns included when building a string piece by piece, and when passing them to an inout std::string (aka std::string&) parameter for a function that should modify the string.

Other patterns I noticed:

  • Loop counters. The above-linked article mentions that range-for reduces these, which is true, but they get used even with range-for when you want to know which iteration of the loop you're in. And 'how do I count my loop iterations with range-for is still a FAQ and has led to standards evolution proposals to directly support it, which is evidence people generally do need to do it.

I stopped partway through... the results weren't encouraging enough to continue the experiment at this time, but I'm open to taking it up again in the future. For now, my results seem to confirm the hypothesis that 'local variables tend to be, well, variables.'

@hsutter
Copy link
Owner

hsutter commented Nov 25, 2023

Named functions are also constant, type also implies constant,

These are inherently static (compile-time) and ODR-necessary in a statically typed language.

Unlike objects, which are inherently run-time dynamic values (and sometimes dynamic types, see previous).

why can't variables be constant too?

(I have to poke a gentle smile at "variables" in the quote here -- I know you mean "objects," but the phrasing "variables be constant" is a little self-answering so I'll point it out just a little bit. 😁 )

Many Cpp2 objects are const by default, including parameters. The ones that are not are function-scope and type-scope objects.

I do think it's interesting that the discussion tends to focus on function scope objects, but not type scope objects. So I have a suggestion... instead of focusing only on function local objects, please also consider type scope objects:

Why (or why not) should type scope objects be const by default, based on their typical usage? And why (or why not) should the default be different for function scope objects, in principle?

@AbhinavK00
Copy link

You've very much convinced me. cpp is a language that doesn't lend itself to have const by default.

Also, const member data doesn't make much sense, they come with problems like making the class's move constructor slower, no std::swap etc. And the better way is to have private members with no public API to change it.

But, can cpp2 still have some terser way to declare const variables?

@JohelEGP
Copy link
Contributor

I do think it's interesting that the discussion tends to focus on function scope objects, but not type scope objects. So I have a suggestion... instead of focusing only on function local objects, please also consider type scope objects:

Why (or why not) should type scope objects be const by default, based on their typical usage? And why (or why not) should the default be different for function scope objects, in principle?

These are the related guidelines that are still relevant in Cpp2:

Type scope objects shouldn't be const by default because most types should be regular.
A function should get to decide whether its objects are const if possible.
The default should be the same for objects in functions, and everywhere, for consistency.

When commit 3512ecd41f33f7e812a070e3370fd97845dd7d24 came around,
I had to change 11 :==s to :=s, for a total of 23 :=s, and managed to keep 5 as :==.
Before that, I had 9 :=s (which excludes : 𝘵𝘺𝘱𝘦 =s).
I could make the copies, which :== didn't necessarily do before.
That's in my GUI code, where most local variables are const.
What I mostly need to modify there are parameters of polymorphic types (possibly the this object).

Now I'm generalizing @array (#797 (comment)).
In writing the logic, I find myself having mostly local variables that I need to populate.
But the pipelines you write with it, and the tests I write for it, are the other way around.
In fact, good pipelines don't even have local variables: #741 (comment).

It seems like how many non-const local variables you have
can be heavily dependent on the paradigms used locally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants