9 ms·
This blog post is not to be taken seriously in my opinion. For one thing, `printf` is obsolete in C++. This is how you should output values in a tagged union:
by clishem 9y ago
This blog post is not to be taken seriously in my opinion. For one thing, `printf` is obsolete in C++. This is how you should output values in a tagged union:
std::variant<size_t, std::string> var = "test";
std::visit([](auto &&val) { std::cout << val; }, var);
If you also want to print the name of the type of the variable, along with the value itself, well, C++ doesn't have reflection (yet). So you're going to have to write a function that takes a value and returns its type as a string, which you can already do using the typeid operator. This is basically implementing reflection yourself, it is cumberstone but doesn't require advanced template programming.
There no need for any of the madness with explicit types inside the visitor lambda, which is deemed as neccesary by the author.
Edit: here's another way to map types to strings. So no templates necessary at all for this entire problem.
std::map<std::type_index, std::string> typeIdxToString;
typeIdxToString[typeid(std::string)] = "string";
typeIdxToString[typeid(int)] = "int";
// ...
Edit2: Turns out C++ has some form of reflection after all. You can just use:
typeid(val).name()
If your compiler supports pretty names.
- dleslie 9y agoHow much stack space does that use, are there allocations and how many are there and how are they managed? These questions seem utterly opaque to me with modern C++. Perhaps I just need to spend a weekend reading the docs again?
- jcelerier 9y ago> sizeof(std::variant<size_t, std::string>) == sizeof(size_t) + sizeof(std::string) + sizeof(discriminant) (at most 4 or 8 I'd guess?) std::variant will never allocate by itself.
- saghm 9y agoShouldn't it be `max(sizeof(size_t), sizeof(std::string)) + sizeof(discriminant)`? At least in other languages, that's how sum types work
- steveklabnik 9y agoI think both of you are also forgetting padding/alignment, though that obviously depends on the exact details.
- jcelerier 9y agofrom what I can see sizeof(std::variant<char, uint8_t>) == 2 so there is no padding between the values and the discriminant
- steveklabnik 9y agoIn this specific case, yes.
- dleslie 9y agoAnd I wasn't clear, in that I was curious about more than the union.
- jcelerier 9y agoyes, forgot the max.
- dleslie 9y agoThat's the union, but what about the algorithm?
- jcelerier 9y agowhy would it allocate ? creating objects does not trigger allocations most of the time. The only stack space used is the internal state of the functor used, which is a lot of time empty (and won't even exist anymore by the time the compiler has optimized your code). eg. look here: https://godbolt.org/g/AdLAi1 https://godbolt.org/g/AdLAi1 there's not a single new / malloc / whatever
- dleslie 9y agoNow I know that there's a stack allocation for the functor. Not all platforms have Sufficiently Smart Compilers; I've got code relying on SDCC and TCC, for instance. :) That's what I'm getting at, there's this whole new suite of functionality that is sort of opaque to me, whereas I'm used to having a grasp of what's going on at-a-glance when working with C and C++98. I'll just have to spend a weekend working with it.
- jcelerier 9y ago> Not all platforms have Sufficiently Smart Compilers; I've got code relying on SDCC and TCC, for instance. :) I'm pretty sure that it's not a matter of compiler brightness; the C and C++ standards are both pretty explicit about when the automatic and dynamic storages are used. If anything, I guess that it would be harder for compilers to allocate such things on the heap. I checked and tcc doesn't allocate anything for instance for this code; neither does gcc 4.4 in c++98 mode : int main() { struct foo { int a, b, c; char x[3000]; } f; }
- dleslie 9y agoYou're basically confirming my supposition that I'll need to spend time reviewing the new C++ standards. ;)
- ChrisSD 9y agoThis works only so long as you can pass the problem on to another function that handles arbitrary types (like `std::cout`). If you want to handle it yourself then you have to use object overloads, templates, etc, etc.
- clishem 9y agoSo basically you're saying that if you want different behavior based the type of a variable passed to a function, you're going to have to write that behavior... Then the answer is yes. We haven't advanced to the point where the compiler can guess that you want. If you can give me an actual example of when it would be an insurmountable task to do so, please let me know.
- ChrisSD 9y agoThe blog post is saying that writing that behaviour is more complex than it needs to be. The author gave an example of pattern matching syntax that would greatly simplify things.
- coldtea 9y ago>So basically you're saying that if you want different behavior based the type of a variable passed to a function, you're going to have to write that behavior... Then the answer is yes. That, and that it should be that hard, and the provided features for doing so should be better, is the entire point of the article. >We haven't advanced to the point where the compiler can guess that you want. That's not some case of magic compilers. This is just bad design. >of when it would be an insurmountable task to do so, please let me know. Whoooosh. The whole point is not that it is insurmountable, but that it's much worse than it should be.
- any1 9y agoThe printing was only a simple example. A less contrived example would be to evaluate an AST. E.g. you would want the '+' operator to do different things for strings and numbers. Also, your visit function does not do exactly the same thing as the author's example. The author's example also prints the type's name. How would you do that without making the visitor cases explicit?
- clishem 9y ago> How would you do that without making the visitor cases explicit? See my edit. std::visit([](auto &&val) { std::cout << typeIdxToString[typeid(val)] << val; }, var); > E.g. you would want the '+' operator to do different things for strings and numbers. Then overload the '+' operator, no need to put all that in the visitor lambda.
- any1 9y agoYes, this would work. I can see that variant may prove useful for the case that you present, but it is much less elegant for the usecase which the author tries to present. Edit: Replaced second "useful" with "elegant"
- geofft 9y ago> printf printf is a good, easy example of a case where you need to have different logic depending on which variant you are. It's not an endorsement of printf qua printf over cout qua cout. cout only works here because the standard library has already overloaded cout for each variant, as the author ends up doing. If you were doing anything else, you'd need either the explicit overloading or the if-constexpr thing. The example from the Rust book (which, full disclosure, I wrote because I <3 tagged enums and pattern-matching and the previous examples weren't that great) is probably a better one: https://doc.rust-lang.org/book/first-edition/enums.html https://doc.rust-lang.org/book/first-edition/enums.html enum Message { Quit, ChangeColor(i32, i32, i32), Move { x: i32, y: i32 }, Write(String), } For these four variants, your behavior is very different. What you want to be able to write is something like loop { match get_message() { Message::Quit => { return; } Message::ChangeColor(r, g, b) => { print!("\027[38;2;{};{};{}m", r, g, b); } Message::Move {x: column, y: row} => { print!("\027[{};{}H", row, column); } Message::Write(str) => { print!("{}", &str); } } } which (modulo any errors from me not actually testing this) is perfectly valid Rust code, that's entirely readable even if you only know C++ and not Rust. As far as I know, you can't write anything anywhere as straightforward as this in C++. You'd need to define at least two new classes, probably four, for the four variants, plus another function that's overloaded on the four classes to handle the behaviors; you can't have the behavior be inline in your existing function, as above. (The way I wrote this, it's just using the global print function, but it'd rapidly get messy if you needed to pass a reference to a console object or whatever.) Alternatively, you would in fact need the constexpr trickery the article suggests, which would let you write it inline with some lambdas. None of this is needed in a language with language-level support for tagged unions and pattern matching (of which Rust is hardly the only one - please don't take this as advocacy of Rust in particular); you can just write normal control structures as above. Alternatively, none of this is needed in a language with dynamic typing; you'd just do while True: msg = get_msg() if msg.type == "Quit": return elif msg.type == "ChangeColor": print "027[38;2;{};{};{}m".format(msg.r, msg.g, msg.b) ... but presumably you're using C++ because you want to be able to do this without that level of dynamism (which puts some unenviable lower bounds on efficiency).
- coldtea 9y ago>This blog post is not to be taken seriously in my opinion. For one thing, `printf` is obsolete in C++. This is how you should output values in a tagged union: This is so irrelevant as to the point of the article that it is funny.
- clishem 9y agoGlad you enjoyed my comment then.