How well do you know C++ auto type deduction?
Posted by volatileint 5 days ago
Comments
Comment by am17an 53 minutes ago
I remember fretting about these rules when reading Scott Meyer's Effective C++11, and then later to realize it's better not to use auto at all. Explicit types are good types
Comment by moregrist 12 minutes ago
Strong agree here. It's not just because it reduces cognitive load, it's because explicit types allows and requires the compiler to check your work.
Even if this isn't a problem when the code is first written, it's a nice safety belt for when someone does a refactor 6-12 months (or even 5+ years) down the road that changes a type. With auto, in the best case you might end up with 100+ lines of unintelligible error messages. In the worst case the compiler just trudges on and you have some subtle semantic breakage that takes weeks or months to chase down.
The only exceptions I like are iterators (whose types are a pita in C++), and lambda types, where you sometimes don't have any other good options because you can't afford the dynamic dispatch of std::function.
Comment by guenthert 39 minutes ago
const auto start = std::chrono::steady_clock::now();
do_some_work(size);
const auto end = std::chrono::steady_clock::now();
const std::chrono::duration<double> diff = end - start;
std::cout << "diff = " << diff << "; size = " << size << '\n';
Looking up the (current standard's) return type of std::chrono::steady_clock::now() and spelling it out would serve no purpose here.Comment by cjfd 30 minutes ago
TP start = TP::clock::now();
do_some_work(size);
TP end = TP::clock::now();Comment by moregrist 9 minutes ago
Comment by UncleMeat 15 minutes ago
Comment by cjfd 36 minutes ago
I also prefer not to use auto when getting iterators from STL containers. Often I use a typedef for most STL containers that I use. The one can write MyNiceContainerType::iterator.
Comment by spot5010 14 minutes ago
auto var = FunctionCall(...);
Then, in the IDE, hover over auto to show what the actual type is, and then replace auto with that type. Useful when the type is complicated, or is in some nested namespace.
Comment by connicpu 8 minutes ago
Comment by maxlybbert 26 minutes ago
Comment by Surac 8 hours ago
Comment by xnorswap 6 hours ago
So for example I'd write:
var x = new List<Foo>();
Because writing: List<Foo> x = new List<Foo>();
Feels very redundantWhereas I'd write:
List<Foo> x = FooBarService.GetMyThings();
Because it's not obvious what the type is otherwise ( Some IDEs will overlay hint the type there though ).Although with newer language features you can also write:
List<Foo> x = new();
Which is even better.Comment by lan321 57 minutes ago
List<Foo> x = new();
since it gives me better alignment and since it's not confused with dynamic.Nowadays I only use
var x = new List<Foo>();
in non-merged code as a ghetto TODO if I'm considering base types/interface.Comment by tcfhgj 5 hours ago
Comment by Leherenn 4 hours ago
With good naming it should be pretty obvious it's a Foo, and then either you know the type by heart, or will need to look up the definition anyway.
With standard containers, you can have the assumption that everyone knows the type, at least high level. So knowing whether it's a list, a vector, a stack, a map or a multimap, ... is pretty useful and avoid a lookup.
Comment by Aeglaecia 5 hours ago
Comment by majoe 8 hours ago
auto it = some_container.begin();
Not even once have I wished to know the actual type of the iterator.
Comment by binary132 1 hour ago
Comment by pjmlp 6 hours ago
IDEs are an invention from the late 1970's, early 1980's.
Comment by 1718627440 41 minutes ago
Having syntax highlighting makes me slightly faster, but I want to still be able to understand things, when looking at a diff or working over SSH and using cat.
Comment by gpderetta 6 hours ago
Comment by pjmlp 5 hours ago
Comment by Rucadi 8 hours ago
For example:
auto a;
will always fail to compile not matter what flags.
int a;
is valid.
Also it prevents implicit type conversions, what you get as type on auto is the type you put at the right.
That's good.
Comment by feelamee 6 hours ago
Comment by jb1991 8 hours ago
Comment by amelius 7 hours ago
Comment by samdoesnothing 8 hours ago
Comment by tigranbs 6 hours ago
Regarding the "auto" in C++, and technically in any language, it seems conceptually wrong. The ONLY use-case I can imagine is when the type name is long, and you don't want to type it manually, or the abstractions went beyond your control, which again I don't think is a scalable approach.
Comment by tialaramex 6 hours ago
In both Rust and C++ we need this because we have unnameable types, so if their type can't be inferred (in C++ deduced) we can't use these types at all.
In both languages all the lambdas are unnameable and in Rust all the functions are too (C++ doesn't have a type for functions themselves only for function pointers and we can name a function pointer type in either language)
Comment by 0xcafecafe 1 hour ago
Comment by 1718627440 5 hours ago
C has this, so I think C++ has as well. You can use a typedef'ed function to declare a function, not just for the function pointer.
Comment by gpderetta 2 hours ago
Comment by 1718627440 2 hours ago
typedef void * (type) (void * args);
type foo;
a = foo (b);
works?Comment by meindnoch 55 minutes ago
A function type describes a function with specified return type. A function type is characterized by its return type and the number and types of its parameters. A function type is said to be derived from its return type, and if its return type is T , the function type is sometimes called ‘‘function returning T’’. The construction of a function type from a return type is called ‘‘function type derivation’’.
Comment by 1718627440 47 minutes ago
> Per ISO/IEC 9899:TC3:
What is it supposed to tell me?
Comment by gpderetta 35 minutes ago
edit: you literally said this in your original comment. I failed at reading comprehension.
Comment by jandrewrogers 9 hours ago
That said, there are some contexts in which “auto” definitely improves the situation.
Comment by jb1991 8 hours ago
Comment by 1718627440 5 hours ago
Comment by adrianN 6 hours ago
Comment by dataflow 9 hours ago
There's nothing special about auto here. It deduces the same type as a template parameter, with the same collapsing rules.
decltype(auto) is a different beast and it's much more confusing. It means, more or less, "preserve the type of the expression, unless given a simple identifier, in which case use the type of the identifier."
Comment by physicsguy 7 hours ago
void func(std::vector<double> vec) {
for (auto &v : vec) {
// do stuff
}
}
Here it's obvious that v is of type double.Comment by wheybags 5 hours ago
Comment by jcelerier 4 hours ago
I've seen much more perf-murdering things being caused by
std::map<std::string, int> my_map;
for(const std::pair<std::string, int>& v: my_map) {
...
}
than with auto thoughComment by dubi_steinkek 2 hours ago
Is it that iterating over map yields something other than `std::pair`, but which can be converted to `std::pair` (with nontrivial cost) and that result is bound by reference?
Comment by nemetroid 52 minutes ago
std::pair<const std::string, int>
vs std::pair<std::string, int>Comment by 1718627440 39 minutes ago
Comment by nemetroid 8 minutes ago
Comment by gpderetta 24 minutes ago
Comment by spacechild1 5 hours ago
Is it really? I rather think that a missing & is easier to spot with "auto" simply because there is less text to parse for the eye.
> If you see "for (auto v : vec)" looks good right?
For me the missing & sticks out like a sore thumb.
> It's easy to forget (or not notice) that auto will not resolve to a reference in this case
Every feature can be misused if the user forgets how it works. I don't think people suddenly forget how "auto" works, given how ubiquitous it is.
Comment by tialaramex 3 hours ago
Comment by gpderetta 2 hours ago
If auto deduced reference types transparently, it would actually be more dangerous.
Comment by spacechild1 2 hours ago
Comment by CalChris 10 hours ago
Comment by Maxatar 10 hours ago
Type inference is usually reserved for more general algorithms that can inspect not only how a variable is initialized, but how the variable used, such as what functions it's passed into, etc...
Comment by zarzavat 9 hours ago
In a modern context, both would be called "type inference" because unidirectional type inference is quite a bit more common now than the bidirectional kind, given that many major languages adopted it.
If you want to specify constraint-based type inference then you can say global HM (e.g. Haskell), local HM (e.g. Rust), or just bidirectional type inference.
Comment by plq 10 hours ago
Comment by gpderetta 6 hours ago
95%[1] of the time, deduction is enough, but occasionally you really wish you had proper inference.
[1] percentage made up on the spot.
Comment by jb1991 8 hours ago
Comment by flakes 9 hours ago
Comment by OneDeuxTriSeiGo 9 hours ago
For any seriously templated or metaprogrammed code nowadays a concept/requires is going to make it a lot more obvious what your code is actually doing and give you actually useful errors in the event someone is misusing your code.
Comment by jjmarr 9 hours ago
Comment by gpderetta 2 hours ago
I have been programming in C++ for 25 years, so I'm so used to the original syntax that I don't default to auto ... ->, but I will definitely use it when it helps simplify some complex signatures.
Comment by dataflow 9 hours ago
Comment by OneDeuxTriSeiGo 3 hours ago
1. Consistency across the board (places where it's required for metaprogramming, lambdas, etc). And as a nicety it forces function/method names to be aligned instead of having variable character counts for the return type before the names. IMHO it makes skimming code easier.
2. It's required for certain metaprogramming situations and it makes other situations an order of magnitude nicer. Nowadays you can just say `auto foo()` but if you can constrain the type either in that trailing return or in a requires clause, it makes reading code a lot easier.
3. The big one for everyday users is that trailing return type includes a lot of extra name resolution in the scope. So for example if the function is a member function/method, the class scope is automatically included so that you can just write `auto Foo::Bar() -> Baz {}` instead of `Foo::Baz Foo::Bar() {}`.
Comment by dataflow 1 minute ago
Comment by Rucadi 7 hours ago
Comment by rovingeye 9 hours ago
Comment by themafia 9 hours ago
Comment by Traubenfuchs 6 hours ago
Comment by 112233 9 hours ago
Comment by OneDeuxTriSeiGo 9 hours ago
Most of the "ugly" of these examples only really matters for library authors and even then most of the time you'd be hard pressed to put yourself in these situations. Otherwise it "just works".
Basically any adherence to a modicum of best practices avoids the bulk of the warts that come with type deduction or at worst reduces them to a compile error.
Comment by 112233 9 hours ago
Comment by OneDeuxTriSeiGo 3 hours ago
Those errors are essentially the compiler telling you in order:
1. I tried to do this thing and I could not make it work.
2. Here's everything I tried in order and how each attempt failed.
If you read error messages from the top they make way more sense and if reading just the top line error doesn't tell you what's wrong, then reading through the list of resolution/type substitution failures will be insightful. In most cases the first few things it attempted will give you a pretty good idea of what the compiler was trying to do and why it failed.
If the resolution failures are a particularly long list, just ctrl-f/grep to the thing you expected to resolve/type-substitute and the compiler will tell you exactly why the thing you wanted it to use didn't work.
They aren't perfect error messages and the debugging experience of C++ metaprogramming leaves a lot to be desired but it is an order of magnitude better than it ever has been in the past and I'd still take C++ wall-o-error over the extremely over-reductive and limited errors that a lot of compilers in other languages emit (looking at you Java).
Comment by 112233 41 minutes ago
But "I tried to do this thing" error is completely useless in helping to find the reason why the compiler didn't do the thing it was expected to do, but instead chose to ignore.
Say, you hit ambiguous overload resolution, and have no idea what actually caused it. Or, conversely, implicit conversion gets hidden, and it helpfully prints all 999 operator << overloads. Or there is a bug in consteval bool type predicate, requires clause fails, and compiler helpfully dumps list of functions that have differet arguments.
How do you debug consteval, if you cannot put printf in it?
Not everyone can use clang or even latest gcc in their project, or work in a familiar codebase.
Comment by ux266478 1 hour ago
Modern C++ in general is so hostile to debugging I think it's astounding people actually use it.
Comment by wheybags 5 hours ago
Comment by 112233 22 minutes ago
Comment by 1718627440 5 hours ago
Citation needed. This is common for embedded application, since why would anyone program a STL for that?
Comment by OneDeuxTriSeiGo 3 hours ago
And with each std release the particularly nasty parts of std get decoupled from the rest of the library. So it's at the point nowadays where you can use all the commonly used parts of std in an embedded environment. So that means you get all your containers, iterators, ranges, views, smart/RAII pointers, smart/RAII concurrency primitives. And on the bleeding edge you can even get coroutines, generators, green threads, etc in an embedded environment with "pay for what you use" overhead. Intel has been pushing embedded stdlib really hard over the past few years and both they and Nvidia have been spearheading the senders and receivers concurrency effort. Intel uses S&R internally for handling concurrency in their embedded environments internal to the CPU and elsewhere in their hardware.
(Also fun side note but STL doesn't "really" stand for "standard template library". Some projects have retroactively decided to refer to it as that but that's not where the term STL comes from. STL stands for the Adobe "Software Technology Lab" where Stepanov's STL project was formed and the project prior to being proposed to committee was named after the lab.)
Comment by gpderetta 20 minutes ago
The other apocryphal derivation of STL I have heard is "STepanov and Lee".
Comment by 112233 2 hours ago
freestanding requires almost all std library. Please note that -fno-rtti and -fno-exceptions are non-conformant, c++ standard does not permit either.
Also, such std:: members as initializer_list, type_info etc are directly baked into compiler and stuff in header must exactly match internals — making std library a part of compiler implementation
Comment by 1718627440 1 hour ago
I did not know that.
My understanding was that C does not require standard library functions to be present in freestanding. The Linux kernel famously does not build in freestanding mode, since then GCC can't reason about the standard library functions which they want. This means that they need to implement stuff like memcpy and pass -fno-builtin.
Does that mean that freestanding C++ requires the C++ standard library, but not the C standard library? How does that work?
Comment by 112233 28 minutes ago
The "abstract machine" C++ assumes in the standard is itself a deeply puzzling construct. Luckily, compiler authors seem much more pragmatic and reasonable, I do not fear -fno-exceptions dissapearing suddenly, or code that accesses mmapped data becoming invalid because it didn't use start_lifetime_as
Comment by 1718627440 2 minutes ago
> freestanding C++ requires the C++ standard library, but not the C standard library
is true?
> The "abstract machine" C++ assumes in the standard is itself a deeply puzzling construct.
I find the abstract machine to be quite a neat abstraction, but I am also more of a C guy.
Comment by gpderetta 6 hours ago
Comment by am17an 52 minutes ago
Comment by andrepd 5 hours ago
Comment by nurettin 3 hours ago
And lifetime specifiers can be jarring. You have to think about how the function will be used at the declaration site. For example usually a function which takes two string views require different lifetimes, but maybe at call site you would only need one. It is just more verbose.
C++ has a host of complexities that come with header/source splits, janky stdlib improvements like lock_guard vs scoped_lock, quirky old syntax like virtual = 0, a lack of build systems and package managers.
Comment by andrepd 1 hour ago
Anything in any language can be very verbose and confusing if you one-line it or obfuscate it or otherwise write it in a deliberately confusing manner. That's not a meaningful point imo. What you have to do is compare what idiomatic code looks like between the two languages.
C++ has dozens of pages of dense standardese to specify how to initialise an object, full with such text as
> Only (possibly cv-qualified) non-POD class types (or arrays thereof) with automatic storage duration were considered to be default-initialized when no initializer is used. Each direct non-variant non-static data member M of T has a default member initializer or, if M is of class type X (or array thereof), X is const-default-constructible, if T is a union with at least one non-static data member, exactly one variant member has a default member initializer, if T is not a union, for each anonymous union member with at least one non-static data member (if any), exactly one non-static data member has a default member initializer, and each potentially constructed base class of T is const-default-constructible.
For me, it's all about inherent complexity vs incidental complexity. The having to pay attention to lifetimes is just Rust making explicit the inherent complexity of managing values and pointers thereof while making sure there isn't concurrent mutation, values moving while pointers to them exist, and no data races. This is just tough in itself. The aforementioned C++ example is just the language being byzantine and giving you 10,000 footguns when you just want to initialise a class.
Comment by 1718627440 33 minutes ago
That's just a list of very simple rules for each kind of type. As a C++-phob person, C++ has a lot of footguns, but this isn't one of them.