Ill-Advised C++ Rant, Part 2 (2016)
codersnotes.com
codersnotes.com
In C++17 you can do
#include <array>
std::array myarray{ 4, 5, 6, 7, 8, 9 };
int my_size = myarray.size(); // returns 6
which even deducts the type `std::array<int, 6>` for you. #include <type_traits>
int myarray[] = { 4, 5, 6, 7, 8, 9 };
int x = std::extent<decltype(foo)>::value; // returns 6
> Converting a string to an enumGenerally a bad idea, as it couples a protocol (whatever is feeding you the strings) to symbols in your language. Often, one of those has constraints that the other cannot meet.
> Binary file includes
objcopy -I binary -O elf64-x86-64 -B i386:x86-64:intel in.bin out.o
Not the prettiest, but, why should a language do the linker's job?> FourCC codes
How often does this come up, that you can't write a 10-line class to do what you want? Not to mention the obvious issue of endianness in the example given. Last time I had to do this I was writing a PNG parser, which was not something I should have been doing.
> Types as first-class primitives
I've been coding for 25 years and never needed nor wanted this. I know not everyone agrees, but personally I find reliance on RTTI to be a code smell. (Granted, pre-C++17 it's kind of forced due to lack of a proper variant type.)
(I realize the above is opinionated, but… so is the OP! :)
template <typename Container>
std::size_t size(Container const& c) { return c.size(); }
template <typename T, std::size_t K>
std::size_t size(T (&)[K]) { return K; }Is it intrinsincally bad though ? For instance F# is able to couple actual data files like CSV to symbols and types in the language, and this allows for very efficient development: http://fsharp.github.io/FSharp.Data/library/CsvProvider.html
> I know not everyone agrees, but personally I find reliance on RTTI to be a code smell.
Again, why ? There are plenty of cases where this has proven quite useful. Besides, "types as first-class primitives" does not necessarily imply "run-time": in the C++ spirit it would most certainly be a compile-time only feature.
Of course that breaks once your CSV header names don't match allowable symbol names. Notice the example linked doesn't show accessing the "Adj Close" field. (Presumably there is a way to get at this in F#? There's no way to "escape" characters in symbols in C++ though.)
Sure, it speeds development, but first time a requirement comes down that says "foo_bar field must be named foo-bar", what should be a one-character change suddenly becomes something much more.
> why ? There are plenty of cases where this has proven quite useful.
"Useful" and "bad idea" are not mutually exclusive. A lot of things which are "useful" – i.e., which speed development or save keystrokes – have costs which hamper maintenance. (Think macros.)
RTTI generally implies that your program has logic to do something based on the type of a value, rather than on the meaning of a value. Say I have an object, BasketOfApples, with a field size_of_basket, which can be either an integer or a string. If it's an integer, it refers to the number of apples the basket can hold. If it's a string, it's either "small", "medium", or "large". OK so far.
But now I want my BasketOfApples to also be able to have a size defined as the weight in pounds it can hold. I can't use an integer again, that already has meaning! But I want a numeric type. So I'm forced to either choose a distinct numeric type (say float), or use an enum to distinguish the cases. The former forces me to make a suboptimal design decision. The latter is probably what I should have done in the first place.
So now my BasketOfApples has an enum field, type_of_basket_size, which determines the meaning of the basket_size value. Not only does type_of_basket_size do what RTTI failed to, it obviates the need for it, and is likely a bit more efficient to boot.
(BTW, you see this all the time with C++ constructors. You can't have two constructors which mean different things but accept the same types without using a dummy "tag" argument.)
Languages with tagged variant types (e.g. OCaml, and I presume by extension, F#) don't have this problem and don't need (or even supply) RTTI information, and they get along just fine. To me, using a feature that isn't available or missed in other "serious" languages, which incompletely accomplishes the task to which it's set, is a code smell.
BTW, I recognize that not everyone agrees with me on this point. But I've been coding like RTTI doesn't exist for decades in various languages and not once have I missed it.
> Besides, "types as first-class primitives" does not necessarily imply "run-time": in the C++ spirit it would most certainly be a compile-time only feature.
The OP's example (copying an object without knowing its type at compile time) specifically requires RTTI.
In F# you can escape symbol names with double backticks. In the example linked, accessing the "Adj Close" field would be done like:
firstrow.``Adj Close``I'm not sure about F#, but I'm almost 100% certain that OCaml does have a runtime "tag" to distinguish cases (and indeed does so at runtime when e.g. pattern matching). That would qualify as a type of RTTI in my book.
(There really isn't way around this in general, except for certain trivially optimizable cases like one-constructor types, etc. But even in those cases runtimes may choose to not do the optimization just to have a uniform runtime representation.)
OCaml does have hidden RTTI, but it is not what you think it is. It exists only so the garbage collector can distinguish the various built-in data types, and is completely opaque to user code (save the Obj module). e.g. all structures have the same tag:
# type foo = { a: int; b: string };;
type foo = { a : int; b : string; }
# Obj.tag (Obj.repr { a = 5; b = "foo" });;
- : int = 0
# type bar = { x: int };;
type bar = { x : int; }
# Obj.tag (Obj.repr { x = 1 });;
- : int = 0
# Obj.tag (Obj.repr 123);;
- : int = 1000
# Obj.tag (Obj.repr "hello");;
- : int = 252
In particular, this tag is certainly not used for pattern matching of anything but explicit variants, where it is used to distinguish values, not types, and hence is not RTTI.[1] https://realworldocaml.org/v1/en/html/memory-representation-...
[2] https://caml.inria.fr/pub/docs/oreilly-book/html/book-ora115...
That's certainly runtime-information about the types of values, but I suppose our disagreement may be about the exact meaning of RTTI. After looking at the Wikipedia entry, I see that my intuition doesn't align with the C++ 'definition' of RTTI, either so thanks for correcting that misconception. It actually looks more like Haskell's Typeable in that it (can apply)/(applies) to any type even primitive types. Of course Typeable is opt-in and stored separately from the values, so...
I'm still not sure I understand your objection, but maybe I just haven't read your OP closely enough and/or thought about it long enough.
type foo = Bar of int | Baz of int
And the two variant cases remain distinguishable, despite both having fields of the same type, and both being members of the same type. No information, runtime or otherwise, about the type, is involved. Therefore it is not by any definition RTTI. Pattern-matching is performed solely based on the explicit tag names.Consider that OCaml variants are in effect no different from the standard C pattern of enum + union. And I think we can agree that C has no RTTI whatsoever.
Now if I could write:
type foo = int | string
Then pattern-matching that would require RTTI. But OCaml doesn't support such things.EDIT: Just in case: We do agree that certain programs in dependently typed languages would require such reification of type information, yes? That is, we're talking about a run-time decision about choosing a type (or another type), yes?
I don't know what you mean by "run-time decision about choosing a type (or another type)". Can you give an example of some code that does this?
I also don't know what you mean by dependently typed languages requiring RTTI. Dependent type systems, like HM and other type systems, only constrain programs, they do not enable any behavior. Any program valid in an arbitrary dependently-typed language is still valid if the typing system is removed, rendering the language untyped (and thus devoid of any RTTI). (Specifically, DT adds to HM the ability to write expressions whose type or validity depends on other values. It does not grant ability to introspect those types; that is out of the scope of a type system.) Example: Coq is dependently typed, but has absolutely no notion of RTTI.
The question arises when you have languages that compile to native code: you want your constraints to still be valid when you load new types in your program at run-time.
eg
obj = dlopen("libfoo.so")
factory = /* insert some cast here */ dlsym(obj, "some_factory_function")
a_value = factory.create_value()
some_constrained_function(a_value)The point of a static type system, either HM or a DT system, is to ensure that the types are either known at compile time, or derivable from some other runtime value (e.g. input to a function). This is for two reasons: safety, and the ability to elide RTTI. (Note that a type system forbids you from doing anything with a value with a dependent type until it finds its way into a conditional branch which simplifies its type to a non-dependent one.)
well, exactly: languages are able to do without the cast at all, just extracting type information from the runtime, which is much safer.
Are you saying that dynamic loaders should check the type of dynamically loaded code against the type expected by the code loading it? I agree that's a good idea, but it doesn't require language support for general RTTI, just a type-aware shared-object format and dynamic loader. (Heck, C++ symbol names already incorporate function argument type signatures, so this isn't a huge stretch.)
Are you saying that a runtime shouldn't need to trust that dynamically-loaded code actually implements the interface it claims to? If so, that's an incredibly specific requirement that's really better served by sandboxing or an RPC-like interface (especially in C/C++).
Neat! In the past I've used perl scripts to munge into C source.
FYI, it generates symbols named after the filename:
tlb@london:~/ur$ objcopy -I binary -O elf64-x86-64 -B i386:x86-64:intel /etc/services services.o
tlb@london:~/ur$ objdump -t services.o
SYMBOL TABLE:
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 g .data 0000000000000000 _binary__etc_services_start
0000000000004c95 g .data 0000000000000000 _binary__etc_services_end
0000000000004c95 g *ABS* 0000000000000000 _binary__etc_services_size
And in C++ you can get the contents with: extern const char _binary__etc_services_start;
extern const char _binary__etc_services_end;
string services(&_binary__etc_services_start, &_binary__etc_services_end);Because it's not cross platform. I have no idea how to do the equivalent in Visual Studio.
The sorts of things I've used reflection for:
- logging - getting the name of things' types, turning enums into strings, etc.
- generating debugging UI - for every bool field in your object, you get a check box; for every enum field, a combo box; for every string field, a text box; and so on
- serialization/deserialization - it has limitations, but you can get surprisingly far before you run into them, and for many situations it doesn't matter
- scripting language integration - for every type of interest in your program, find its fields and methods, and link those up to the scripting system
- dependency injection - given a set of known services, when creating an object (perhaps by name, provided at runtime, which you match to a type by scanning the list of types available) provide it with the services it needs (determined by, say, examining the types of the parameters of its constructor). Typically you'd cross-reference types of parameter with types of each service to match them up, but there are various options
Especially good with a C#-like metadata system that lets you assign user-defined properties to fields that your calling code can query to get further info. For example, you might not want to serialize certain fields, or you might want to hide some fields from the UI, and so on.
(If this doesn't convince you, fine; for my money, though, this is the #1 missing feature from C++. But, like the post's author, my background is working on video games... maybe this is just stuff that only video games people would want?)
Can we have basics first, and then robust libraries for say, as simple as networking.
Batteries included, is a thing, and is something a lot of programmers like me look for when choosing between languages.
I don't envy the members of the standards committee. It is a hard and very thankless job.
On the other hand, there's 100 ways to implement, say, an HTTP client, and they're all wrong.
† Except for iostreams. Ugh.
#define my_assert(X) ({ if (!(X)) error(); })
Another way is to use a lambda expression, and then call it immediately: #define my_assert(X) ( [=]() { if (!(X)) error(); } )()
Because the lambda captures by copy, it will catch inadvertent side effects in some cases.You can also use the conditional operator in this case:
#define my_assert(X) ( (X) ? error() : ((void)0) ) #define my_assert(X) ( !(X) ? error() : ((void)0) )> Calculating the size of a static array
some_array.len()
> Getting the maximum value of an enum
Rust's enums are strongly-typed (I think that's the right phrase), so this is a non-issue.
> Converting an enum to a string
impl Display for MyEnum (and then call to_string() on it)
> Converting a string to an enum
This could be tricky, since Rust enum variants can contain data. But if none of the variants contain data, it would be easy to make a method that returns a Result<MyEnum, CustomParseError>
> Binary file includes
include_bytes! macro (or the include_str! macro, depending on the exact scenario)
> Switch/case is still stupid
Pretty sure Rust's match is a lot better
Usually when you want to convert a string to an enum, you're trying to build some kind of parser or deserialization library, in which nom or serde are the best choices. (Of course, C++ has several parser frameworks too.)
1. Size of a static array: Use std::array please
2. Maximum value of an enum: See reflection TS
3. Converting enum to string: ditto
4. Converting string to enum: ditto
5. #pragma once: see modules. They are in fact trying to make something better. There are also some cases where you don't want/need it
6. C99 designated initializers. They are there!
7. Binary file includes. Ugh this is just such a dumb idea I can't even. When do you want the file to load? When the executable does? What if you want to read it in chunks? This idea is not well suited for a systems-oriented language
8. FourCC codes. Again, this is a silly idea not even worth addressing. Just define it yourself. C++ is a programming language. Not your media toolkit
9. do/while(0). What are you using macros for in 2016 (the time of writing)?
10. preprocessor gripes. Stop using it! You have <type_traits> and stuff now
11. Switch/case is stupid. No it's not. You want fall/through cases as well and if you can't see why, don't criticize. This is true in pretty much EVERY language that supports switch/case
12. Still no strong typedefs. There are! Use the explicit keyword on a constructor definition.
13. Types as Primitives. See reflection TS
Basically, this feels like any other "C++ rant." Someone who didn't learn the language properly or respects what the language needs to achieve.
These are hard problems, and the things in 11, 14, and 17 are useful. Ultimately, people [on the committee and developing the language] are prioritizing what they need.
It's not a closed process; it's just a slow one.
Unfortunately, I'm still not 100% this is what they're referring to, but I think this is it.
https://isocpp.org/files/papers/n3996.pdf
EDIT: Here it is :D
https://www.meetingcpp.com/blog/items/reflections-on-the-ref...
Am I a bad C++ programmer for agreeing with some of it?
> 5. #pragma once: see modules. They are in fact trying to make something better. There are also some cases where you don't want/need it
The fact that modules have taken so long, and still have prominent members of the community saying they need to be sent back to the drawing board, is a legitimate criticism.
> 7. Binary file includes. Ugh this is just such a dumb idea I can't even.
It's a useful feature, much used in the language I work on, allowing you to distribute a program that needs just one or two static data files as a single executable. It's also good for packaging data as libraries: for instance, Unicode libraries need a lot of random tables and it can be convenient to store these in binary form. Having to embed binary in C/C++ syntax as, say, ICU does just adds parsing overhead at compile time for no reason.
> 9. do/while(0). What are you using macros for in 2016 (the time of writing)?
There are lots of use cases of macros, too many to list here. Templates don't always suffice.
> 11. Switch/case is stupid. No it's not. You want fall/through cases as well and if you can't see why, don't criticize. This is true in pretty much EVERY language that supports switch/case
I designed much of the pattern matching system in an industry language without fall through in switch case and I have never missed it. Nor has anyone else in the community I know of.
This can be really useful in embedded devices where there is no file system but I don't really see the need for it to be part of the language, every time I've needed it the toolchain came with build tools to handle this.
> 11. Switch/case is stupid. No it's not. You want fall/through cases as well and if you can't see why, don't criticize. This is true in pretty much EVERY language that supports switch/case
a smarter option may have been to require an explicit break or fallthrough keyword but we're a good 40 years late.
>12. Still no strong typedefs. There are! Use the explicit keyword on a constructor definition.
Define a new wrapper class (maybe I'm missing something)? It would be a lot nicer if you could do something like `typedef explicit int FooId;`
For simple types, for which this makes most sense but from which you cannot inherit, it is a lot of work.
"Pretty much every language," right? C++ is right in there with C, JavaScript, and PHP.
This is the biggest thing that is wrong with C++. Everything is always stupid human's fault. :D
constexpr std::uint32_t operator "" _4CC(const char* str, size_t len) {
std::uint32_t result = 0;
for (size_t i = 0; i < len; ++i) {
result = (result << 8) + (std::uint32_t)(unsigned char)(str[i]);
}
return result;
}
auto const movie = "MooV"_4CC;>#define countof(X) (sizeof(X) / sizeof((X)[0]))
Could they? What if X is empty?
struct Widget{
};
Widget widgets[256];
printf("%d %d", sizeof(widgets), sizeof(Widget));
prints out 256 1
if there are 0 elements, there are no issues because it's 0/x = 0.It's generally a bad idea to do this, and an enormous headache to even attempt. If you want to do this, maybe you don't want to use an enum, because that's not what they are for. Maybe you want a hash map instead?
I wonder how much effort might be required to implement a purely opt-in syntax/library for the purpose: something like Ada's Enumeration_IO.
cout<<nameof(Dog)
The compiler could just replace it with the string value at compile time.If you have a dynamic one the compiler could just make an array (or worst case hash table lookup) into a static array created at compile time. It's fairly simple and costs nothing if you don't use it. And it's a lot simpler and more elegant to do through the compiler than through a lib, metaprogramming etc.
And how useful is it? it's at least useful to be part of how most other languages work these days. An enum is should be seen as a list of names and values.
It shouldn't just be limited to be a glorified way of saying
const int Cat = 0;
const int Dog = 1;
Defined like that I'd find it completely unnatural if the symbol names were available at runtime.Something like this ought to be opt-in and explicit.