Open MatthieuMv opened 8 months ago
and function arguments with const by default
This is already the case for function arguments, which default to pass-by in
.
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, https://github.com/hsutter/cppfront/issues/386#issuecomment-1518194517).
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 (β¦) {
β¦
}
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
Instead of switching to the default and requiring mut, or similar, perhaps :== could imply const?
On 24 November 2023 14:44:36 Nick Treleaven @.***> wrote:
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, Rockethttps://github.com/SergioBenitez/Rocket, had about 8%.
https://verdagon.dev/blog/on-removing-let-let-mut#why-we-like-it
β Reply to this email directly, view it on GitHubhttps://github.com/hsutter/cppfront/issues/815#issuecomment-1825771040, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AALUZQNPPJRWWZMMX6JJL53YGCXFBAVCNFSM6AAAAAA7IB4US2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQMRVG43TCMBUGA. You are receiving this because you are subscribed to this thread.Message ID: @.***>
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?
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.
type
implies const
in t: @struct type = { }
.
Much like Cpp2 v :== 0;
and Cpp1 constexpr int v = 0;
imply const
for v
.
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.
I also don't know how that extends to functions
Named functions are also constant.
Named functions are also constant, type
also implies constant, why can't variables be constant too?
They can be constant.
It's just not implied by default.
You need to explicitly declare them const
.
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:
std::variant
and std::any
, tend to be set after construction.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.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:
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.'
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?
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?
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:
const
to define objects with values that do not change after construction.const
or constexpr
unless you want to modify its value later on.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
(https://github.com/hsutter/cppfront/discussions/797#discussioncomment-7622822).
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: https://github.com/hsutter/cppfront/issues/741#issuecomment-1754194152.
It seems like how many non-const
local variables you have
can be heavily dependent on the paradigms used locally.
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.