Remix.run Logo
pjmlp 6 hours ago

Some C devs will make all kinds of crazy efforts only not to use C++.

greenavocado 4 hours ago | parent [-]

C++ is edge case hell even for simple looking code

pjmlp 4 hours ago | parent [-]

Not really, only in the mind of haters.

greenavocado 2 hours ago | parent [-]

Let's start with object construction. You think you're creating an object. The compiler thinks you're declaring a function.

    Widget w();  // I made a widget, right? RIGHT?
Wrong. You just declared a function that takes no parameters and returns a Widget. The compiler looks at this line and thinks "Ah yes, clearly this person wants to forward-declare a function in the middle of their function body because that's a completely reasonable thing to do."

Let's say you wise up and try this:

    Widget w(Widget());  // Surely THIS creates a widget from a temporary?
Nope! That's ALSO a function declaration. You just declared a function called w that takes a function pointer (which returns a Widget) as a parameter.

The "fix"? Widget w{}; (if you're in C++11 or later, and you like your initializers curly). Widget w = Widget(); (extra verbose). Widget w; (if your object has a default constructor, which it might not, who knows).

The behavior CHANGES depending on whether your Widget has an explicit constructor, a default constructor, a deleted constructor, or is aggregate-initializable. Each combination produces a different flavor of chaos.

--

So you've successfully constructed an object. Now let's talk about copy elision, where the language specification essentially shrugs and says "the compiler might copy your object, or it might not, we're not going to tell you."

    Widget makeWidget() {
        Widget w;
        return w;  // Does this copy? Maybe! Does it move? Perhaps! Does it do neither? Could be!
    }
Pre-C++17, this was pure voodoo. The compiler was allowed to elide the copy, but not required to. So your carefully crafted copy constructor might run, or it might not. Your code's behavior was non-deterministic.

"But we have move semantics now!" Return Value Optimization (RVO) and Named Return Value Optimization (NRVO) are not guaranteed, depend on compiler optimization levels, and can be foiled by doing things as innocent as having multiple return statements or returning different local variables.

    Widget makeWidget(bool flag) {
        Widget w1; 
        Widget w2;
        return flag ? w1 : w2;  // NRVO has left the chat
    }
Suddenly your moves matter again. Or do they? Did the compiler decide to be helpful today? Who knows! It's a surprise every time you change optimization flags!

--

C++11 blessed us with auto, the keyword that promises to save us from typing out std::vector<std::map<std::string, std::unique_ptr<Widget>>>::iterator for the ten thousandth time. Most of the time, auto works fine. But it has opinions. Strong opinions. About const-ness and references that it won't tell you about until runtime when everything explodes.

    std::vector<bool> v = {true, false};
    auto x = v[0];  // x is not bool. x is std::vector<bool>::reference, a proxy object
    x = false;
    // v[0] is now... wait, what? Did that work? Maybe! If x hasn't been destroyed!

    const std::string& getString();
    auto s = getString();  // s is std::string (copy made), NOT const std::string&
You wanted a reference? Too bad! Auto decays it to a value. You need auto& or const auto& or auto&& (universal reference! another can of worms!) depending on your use case. The simple keyword auto has spawned a cottage industry of blog posts explaining when you need auto, auto&, const auto&, auto&&, decltype(auto), and the utterly cursed auto*.