Enlightened



  • My first thought was also the CRTP, that seems to be the only way it makes (some degree of?) sense.

    @cvi said in Enlightened:

    @djls45 It doesn't enforce the singletoniness, but at least it won't blow up in your face. (I mean, it's still useless, but at least it isn't actively dangerous that way.)

    I guess that the idea is that you can use the singleton syntax to access it. Still not a real singleton, but at least you can use it as a singleton, if you already know it is one. It's some kind of coding by contract, i.e. the equivalent of the first coder saying "pretty please (*), only use that class as a singleton".

    (*) or less polite "you'll be fired if you don't do it!" versions...


  • Discourse touched me in a no-no place

    @remi said in Enlightened:

    I guess that the idea is that you can use the singleton syntax to access it.

    Doing singleton-ness via a template is reasonable (assuming that you accept that Singleon is a good pattern in the first place) but embedding it within that class's own class hierarchy is not, since new and other types of object construction aren't overridable that way in C++ (as they have their own contracts to obey). It's once you get into this sort of thing that switching to DI/IoC starts to make a lot of sense.



  • @Medinoc said in Enlightened:

    Then you get pointers to member variables, which are basically offsets into the object...

    Except when they aren't. Example: classes D1, D2, and D3 derive "virtually" from a common base class B. Classes DD123 and DD321 derive normally from, respectively, D1, D2, D3; and D3, D2, D1. All classes have their own data members and the data members of their base classes.

    What is the offset in D1 of D1::d1? It turns out that you can't give a unique answer without knowing which of DD123 and DD321 contains the D1. (Well, maybe you can, but if you can, then you can't tell the offset of D1::b (the member variable of B::b seen through the lens of D1).)



  • Addendum. I suppose the conclusion that we can draw is that you should not assume anything about the size and structure of a pointer in C, and even less so in C++. Well, except for the aforementioned detail that during the execution of a program, the size of each pointer variable is fixed.


  • FoxDev

    @Steve_The_Cynic said in Enlightened:

    I suppose the conclusion that we can draw is that you should not assume anything about the size and structure of a pointer in C, and even less so in C++.

    Let's face it: there's so many platform-specific definitions and so much undefined behaviour in both languages, that assuming anything is going go blow up in your face eventually.


  • Impossible Mission - B

    @RaceProUK

    1⃣1⃣0⃣0⃣: Did you just assume my pointer size? TRIGGERED! *crash*


  • area_can

    @dcon said in Enlightened:

    Formatting your disk is a valid option.

    Also, nasal demons!



  • @RaceProUK said in Enlightened:

    Let's face it: there's so many platform-specific definitions and so much undefined behaviour in both languages, that assuming anything is going go blow up in your face eventually.

    The annoying thing is that you don't really need to assume anything. Most of the undefined behavior emerges when people try to be clever. Doing things in the most straightforward manner is generally perfectly safe and efficient. And there are ways to statically check any assumptions you make so that they're no longer assumptions.



  • @RaceProUK said in Enlightened:

    @Steve_The_Cynic said in Enlightened:

    I suppose the conclusion that we can draw is that you should not assume anything about the size and structure of a pointer in C, and even less so in C++.

    Let's face it: there's so many platform-specific definitions and so much undefined behaviour in both languages, that assuming anything is going go blow up in your face eventually.

    Up to a point. There's a debate about whether "writing code as if the size limits imposed by the standards are supported by the compiler" counts as making an assumption.

    Case in point: when is the destructor called for an "auto" storage class object created inside a naked brace block (i.e. no if/while/for/do...while by it)? If you say, "Well, obviously at the end of the brace block," I'll introduce you to Sun's C++ compiler (v8, I think it was) where the object hung around until the end of the function the naked brace block was in. (Of course, that might make it a compiler for a different language that has the same syntax as C++.) Or I could mention IBM's C++ for AIX that worked together with the linker to delete unreferenced global variables, even objects with substantive constructors and destructors (and whose absence was therefore detectable).


  • Winner of the 2016 Presidential Election

    @Steve_The_Cynic said in Enlightened:

    Sun's C++ compiler

    Ouch! The memories, they hurt!

    OTOH, Sun is the only compiler whose standard library correctly puts the types from cstdint in the std namespace. So at least they do some things correctly, which is nice.



  • @Steve_The_Cynic said in Enlightened:

    I'll introduce you to Sun's C++ compiler (v8, I think it was) where the object hung around until the end of the function the naked brace block was in

    Well if you're going down that rabbit hole, you're now in a state where you can't assume anything about anything, because the compiler might not have implemented the language as it described in the standard. Are you suggesting we only write C++ code that cannot possibly be miscompiled by a compiler that has a bug in it?



  • @asdf said in Enlightened:

    OTOH, Sun is the only compiler whose standard library correctly puts the types from cstdint in the std namespace. So at least they do some things correctly, which is nice.

    Isn't this one of the things that the standard ended up relaxing around C++11? I.e. it's now permissible to put the C-API stuff both inside and outside of std, as long as you do have it in std for the <cxyz> versions, and outside for the <xyz.h> versions. (Recent GCC/Clang/MSVC do have the cstdint types in std ... as well.)

    @gwowen said in Enlightened:

    because the compiler might not have implemented the language as it described in the standard.

    <cynical>Wasn't this the default assumption pre-C++11?</cynical> (At least the major compilers seem to be getting their shit together with C++11 and newer standards.)


  • Winner of the 2016 Presidential Election

    @cvi said in Enlightened:

    Isn't this one of the things that the standard ended up relaxing around C++11?

    Yeah, unfortunately. (IIRC, C++14 was the first version that officially allowed putting those types in the global namespace.)

    I.e. it's now permissible to put the C-API stuff both inside and outside of std, as long as you do have it in std for the <cxyz> versions, and outside for the <xyz.h> versions.

    It's still stupid. The only valid reason (IMO) to allow those types outside of std is backwards compatibility with C programs. That's covered by providing the stdint.h header. Why even have a separate cstdint header for C++ programs if that is allowed to pollute the global namespace as well? Just so that sloppy programmers don't have to change that one include?



  • @gwowen said in Enlightened:

    @Steve_The_Cynic said in Enlightened:

    I'll introduce you to Sun's C++ compiler (v8, I think it was) where the object hung around until the end of the function the naked brace block was in

    Well if you're going down that rabbit hole, you're now in a state where you can't assume anything about anything, because the compiler might not have implemented the language as it described in the standard. Are you suggesting we only write C++ code that cannot possibly be miscompiled by a compiler that has a bug in it?

    Not at all. (And I suspect this was deliberately built this way. Certainly the IBM one was. So it's still a bug, in that it claims to be a C++ compiler or compiler/linker system, while simultaneously not being conformant. But there's a distinct possibility that people interpret "bug" as meaning "behaviour inadvertently inserted by a programmer who made a mistake" rather than "behaviour deliberately designed into the software".)



  • @asdf said in Enlightened:

    It's still stupid. The only valid reason (IMO) to allow those types outside of std is backwards compatibility with C programs.

    There's some handwaving weirdness about how you link programs that include <cstdio> and use (for any reason, even bad ones(1)) things like stdin or stdout.

    (1) That's without discussing the documented consideration that, bad and slow as printf and friends are, they are still much faster and easier to use than iostreams.



  • @cvi said in Enlightened:

    <cynical>Wasn't this the default assumption pre-C++11?</cynical> (At least the major compilers seem to be getting their shit together with C++11 and newer standards.)

    I have a dim recollection of discussions about why exactly Visual C++ 6 didn't correctly (according to C++98) handle variables defined inside the parentheses of a for, if, or while. Apparently, the draft standard went back and forth between the variables existing after the statement and being restricted to the statement, and Microsoft chose the wrong moment to finalise the language the compiler would support.


  • Winner of the 2016 Presidential Election

    @Steve_The_Cynic said in Enlightened:

    iostreams

    i.e. the library that is broken by design because it doesn't allow you to create translatable format strings. Performance isn't even relevant, iostreams are broken by design.



  • @asdf said in Enlightened:

    @Steve_The_Cynic said in Enlightened:

    iostreams

    i.e. the library that is broken by design because it doesn't allow you to create translatable format strings. Performance isn't even relevant, iostreams are broken by design.

    Yeah, well, that, too. But they are horribly, horribly slow. Also: translatable format strings are something of a black art in their own right. Linguisto-culturally "sane" formatting of dates and even numbers is ... hard. I once wrote code to use the Windows cultural formatting APIs for formatting numbers. I still get the willies thinking about it. There isn't (more accurately, I didn't find) a simple way to comma-fy a formatted number, and correctly interpreting the data returned by the APIs is ... interesting ... mostly because of the way that the Indians handle this.(1)

    (1) The least-significant group is three digits, and the other groups are two digits each. The Windows APIs are sufficiently rich to express this - and a bunch of more complex things - but it's really annoying to use.



  • @asdf said in Enlightened:

    iostreams are broken by design.

    Plus they seem to follow an "all text and no binary" approach, with anything binary added as afterthought (nothing for serializing an int or int32_t, let alone anything else, and I was once told that ios::binary isn't always enough to make sure you have a stream that actually saves your data unmolested: According to the guy, the default locale can still screw you over if it was set by a troll).

    And it's still prevalent: Just look at the <random> classes, that can be serialized as text but provide absolutely nothing for binary serialization: Congratulations, you just tripled the size of your Mersenne twister state!



  • @asdf said in Enlightened:

    It's still stupid. The only valid reason (IMO) to allow those types outside of std is backwards compatibility with C programs. That's covered by providing the stdint.h header.

    Yeah, I agree. I can understand why they decided to do it that way, but, yeah :-/


  • Impossible Mission - B

    @Medinoc said in Enlightened:

    Plus they seem to follow an "all text and no binary" approach, with anything binary added as afterthought (nothing for serializing an int or int32_t, let alone anything else, and I was once told that ios::binary isn't always enough to make sure you have a stream that actually saves your data unmolested: According to the guy, the default locale can still screw you over if it was set by a troll).

    And it's still prevalent: Just look at the <random> classes, that can be serialized as text but provide absolutely nothing for binary serialization: Congratulations, you just tripled the size of your Mersenne twister state!

    This makes more sense (as in being understandable, not as in being sensible) when you remember that C comes from the same folks who gave us the Unix Philosophy. 🚎



  • @masonwheeler Not sure about the <random> classes, but isn't memcpy how you serialize and deserialize stuff in binary? The << and >> operators are for formatted input and output, meaning text. The streams have read and write methods for unformated input and output. If your class has pointers of course you need to do your own "ToBinary" method, but that's no different from overloading the formatted output operators for your class.

    So if you want to write an int to a stream in binary, you would do:

    int myInt { 10 };
    std::cout.write(reinterpret_cast<char*>(&myInt), sizeof(myInt));
    

    and to get it out:

    int myInt;
    std::cin.read(reinterpret_cast<char*>(&myInt), sizeof(myInt));
    


  • @Kian said in Enlightened:

    @masonwheeler Not sure about the <random> classes, but isn't memcpy how you serialize and deserialize stuff in binary? The << and >> operators are for formatted input and output, meaning text. The streams have read and write methods for unformated input and output. If your class has pointers of course you need to do your own "ToBinary" method, but that's no different from overloading the formatted output operators for your class.

    So if you want to write an int to a stream in binary, you would do:

    int myInt { 10 };
    std::cout.write(reinterpret_cast<char*>(&myInt), sizeof(myInt));
    

    and to get it out:

    int myInt;
    std::cin.read(reinterpret_cast<char*>(&myInt), sizeof(myInt));
    

    You used reinterpret_cast. Instant code smell. (Sure, I get it, you may have to use it, but it's still code smell.)


  • Winner of the 2016 Presidential Election

    @Steve_The_Cynic said in Enlightened:

    You used reinterpret_cast. Instant code smell.

    Not if you're casting a pointer to char* or void*. Looks perfectly fine to me.



  • @Medinoc said in Enlightened:

    and I was once told that ios::binary isn't always enough to make sure you have a stream that actually saves your data unmolested: According to the guy, the default locale can still screw you over if it was set by a troll

    Well... basic_ostream::write:

    When using a non-converting locale (the default locale is non-converting), the overrider of this function in std::basic_ofstream may be optimized for zero-copy bulk I/O (by means of overriding std::streambuf::xsputn)

    ... indicating that a converting locale might mess with your data. If understand things correctly, basic_ios::init initializes the locale to std::locale(), which can be overridden with std::locale::global().

    So, yeah, seems like this has some amount of trolling potential.



  • @asdf said in Enlightened:

    @Steve_The_Cynic said in Enlightened:

    You used reinterpret_cast. Instant code smell.

    Not if you're casting a pointer to char* or void*. Looks perfectly fine to me.

    Except, you don't need to cast to (cv-qualified) void* because the conversion is implicit. (So, why are the C++ streams using char-pointers? They're not - it's actually char_type, the template type passed to basic_whateverstream. The ones that aren't char-based (like w?stream) probably can't even do binary IO).

    I'll stick to the C-API, it's less insane. You can't extend it (via standard methods) to new stream types, but then again, the hoops that you have to go through to make this work in C++ isn't exactly pretty either (the streambuf mess).


  • Java Dev

    @cvi said in Enlightened:

    You can't extend it (via standard methods) to new stream types

    Though there are nonstandard methods, like glibc's fopencookie().



  • @Steve_The_Cynic said in Enlightened:

    You used reinterpret_cast. Instant code smell. (Sure, I get it, you may have to use it, but it's still code smell.)

    The cast to char* is specifically allowed for the case where you want to access the binary representation of an object, and when serializing an object in binary we want access to the binary representation of the object. So yes, while using reinterpret cast is a flag that you're doing something dangerous, this is a case of using it for one of the specific uses it has. Yeast is also smelly but you can't make cake or beer without it.

    @cvi said in Enlightened:

    The ones that aren't char-based (like w?stream) probably can't even do binary IO).

    No, but then, the stream classes model streams. Binary IO can be modeled as a stream of chars, but it can't be modeled as a stream of wide chars.


  • Winner of the 2016 Presidential Election

    @Kian said in Enlightened:

    Yeast is also smelly but you can't make cake or beer without it.

    You put yeast in cheesecake? :doing_it_wrong:



  • @asdf Cheesecake is not cake, just like beefcake is not cake.



  • @Kian said in Enlightened:

    No, but then, the stream classes model streams. Binary IO can be modeled as a stream of chars, but it can't be modeled as a stream of wide chars.

    Sure, but that's bloody backwards. Binary IO is the more fundamental mode, where a binary stream (a stream of chars as in "chars that you use to access the binary representations of objects) can support both a stream of narrow chars (as in "chars that are part of a string") and wide chars. The backward behaviour is one of the consequences of the overloaded meaning of char in C++ -- if we had separate types for the two, the interface wouldn't even be possible (or at least it'd be unusable for binary IO).

    Also, to paraphrase a complaint about iostreams: if you're only doing binary IO, why the fuck do you need to use a class that knows about locales?



  • @cvi What's backwards about it? There's a strict progression of
    data --> binary representation of data --> interpreted data

    The stream classes can work on binary data or interpreted data, but once the data has been interpreted, it's really hard to go back to the binary data, and nearly impossible to guarantee that it would always be the same as the original input. char, signed char, and unsigned char are each defined as a single byte, which is the only way to guarantee bit-level reading of data in order, especially when handling the endian-ness of words (getting into the interpreted level) in different systems.

    Using the binary mode of a stream tells the class to skip the interpretation step. However, a wchar_t (or other "wide" "character" types) is implicitly an interpretation of the binary data. char is not defined to include wide character handling.


  • Winner of the 2016 Presidential Election

    @cvi said in Enlightened:

    The backward behaviour is one of the consequences of the overloaded meaning of char in C++ -- if we had separate types for the two, the interface wouldn't even be possible

    Welcome to C++17, which has exactly what you want:



  • @asdf said in Enlightened:

    @cvi said in Enlightened:

    The backward behaviour is one of the consequences of the overloaded meaning of char in C++ -- if we had separate types for the two, the interface wouldn't even be possible

    Welcome to C++17, which has exactly what you want:

    ... which is basically just a wrapper over a char with reduced functionality in order to please :pendant:


  • Winner of the 2016 Presidential Election

    @djls45
    It allows you to make your APIs and code more self-documenting, which is a good enough reason to use it.



  • @asdf I guess.

    But if you want readable, self-documenting code, why would you choose C++ in the first place? 🎣
    I actually like C++.


  • Winner of the 2016 Presidential Election

    @djls45 said in Enlightened:

    But if you want readable, self-documenting code, why would you choose C++ in the first place?

    Because it allows you to put all the non-obvious code into a evil_templates.tpp header file and then write readable, self-documenting code everywhere else? 🐠
    Spoiler alert: This is a very bad idea!

    Seriously, though: The separation between interface (header files) and implementation (code files) - among other things - actually makes (well-written) C++ easier to read than well-written Java, in my very personal opinion.



  • @asdf is that with or without consideration of text editor fold support? You could fold all the functions in a class in Java for a C++ header-style view.


  • Winner of the 2016 Presidential Election

    @LB_ said in Enlightened:

    You could fold all the functions in a class in Java for a C++ header-style view.

    But then you'd still see all the helper functions, which would be in an anonymous namespace in the implementation file in C++.


  • Discourse touched me in a no-no place

    @Kian said in Enlightened:

    Cheesecake is not cake, just like beefcake is not cake.

    Beefcake is not cattle cake.



  • @asdf said in Enlightened:

    Welcome to C++17, which has exactly what you want:

    Oh. Didn't realize that actually had made it into standard. Yay!


  • Fake News

    @cvi Now you only got to wait until 2040 for it to make it into your favorite compiler.


  • Winner of the 2016 Presidential Election

    @JBert Didn't make it into Visual Studio 2017, apparently: https://docs.microsoft.com/en-us/cpp/visual-cpp-language-conformance

    But the situation has improved a lot, I'd expect the next version to implement almost all of the C++17 library features.



  • @asdf said in Enlightened:

    Seriously, though: The separation between interface (header files) and implementation (code files) - among other things - actually makes (well-written) C++ easier to read than well-written Java, in my very personal opinion.

    Unless you're writing lots of template stuff, then your implementation will be in a mix of header and code files. That annoys my OCD.


  • Winner of the 2016 Presidential Election

    @NedFodder said in Enlightened:

    Unless you're writing lots of template stuff, then your implementation will be in a mix of header and code files. That annoys my OCD.

    You can still separate them, just give the implementation a different file ending (e.g. tpp) and include them from the header file.



  • @Kian said in Enlightened:

    @Steve_The_Cynic said in Enlightened:
    The cast to char* is specifically allowed for the case where you want to access the binary representation of an object, and when serializing an object in binary we want access to the binary representation of the object. So yes, while using reinterpret cast is a flag that you're doing something dangerous, this is a case of using it for one of the specific uses it has. Yeast is also smelly but you can't make cake or beer without it.

    It's still highly codesmelly because it forces the data to be saved in host endianness, rather than specifying one.

    Which brings me to a weirdness of .Net: BitConverter uses only host endianness, while BinaryWriter guarantees little-endian...



  • @asdf said in Enlightened:

    @Steve_The_Cynic said in Enlightened:

    You used reinterpret_cast. Instant code smell.

    Not if you're casting a pointer to char* or void*. Looks perfectly fine to me.

    I would normally prefer an implicit cast to void *, or maybe static_cast<void *>. In extreme circumstances, if the source pointer points to a class, I'd even accept someone doing dynamic_cast<void *>. (That is a thing. It explores up the object's true inheritance hierarchy to find the most derived class, the true outermost type of the object, and returns a pointer to that.)

    But if he's doing that, I'd also expect a really, really good explanation of why.


  • Winner of the 2016 Presidential Election

    @Steve_The_Cynic said in Enlightened:

    static_cast<void *>

    I guess it's a matter of personal preference whether you'd prefer a static_cast or reinterpret_cast in this case. The result is the same.

    And of course there's at least one discussion on SO:



  • @asdf said in Enlightened:

    @Steve_The_Cynic said in Enlightened:

    static_cast<void *>

    I guess it's a matter of personal preference whether you'd prefer a static_cast or reinterpret_cast in this case. The result is the same.

    It is, but static_cast does at least have some ability to police what it's doing. Under no circumstances, for example, should you permit yourself to reinterpret_cast to get from a derived class pointer to a base class pointer, or vice versa. If you do, you'll end up with a crater in the middle of your face that marks the place your nose was before the demons flew out of it. (This is a fancy way of saying that reinterpret_cast should be restricted to those places where it is necessary, and not used at any other time.)



  • @asdf said in Enlightened:

    And of course there's at least one discussion on SO:

    Wow. The first answer says "reinterpret_cast has lots of advantages, that's why I use static_cast". The second answer says "static_cast has lots of advantages, that's why I use reinterpret_cast". I love SO sometimes.


Log in to reply