C++ with Simon

Started by Simon, June 28, 2022, 10:56:23 PM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

Simon

These are the C++-only posts from Simon blogs.




Apropos uniform initialization syntax. :8():

I've used C++ for 16 years now and I still don't know every case of where the language guarantees a zero initialization and when it may leave something uninitialized.

Everybody learns early on that this int is uninitialized:

void foo() {
    int a1;
}


And that's about as far as "everybody knows" goes, I'd wager. :lix-evil: Only many people, including me, know that these two are zero-initialized (although I'd still add the explicit = 0):

int a2; // at global scope
void bar() {
    static int a3;
}


The design reason behind this is that these ints live in "static land" instead of going on the stack where the zero initialization would cost runtime: The BSS section is a memory region that comes preinitialized for free when the operating system loads your executable and has to copy all of the executable code into memory anyway.

But I have no clue of the following, although I see this occasionaly in the day job, usually in C code that somebody renamed to .cpp later:

void baz() {
    int arr1[5] = {};
// Is anything zero?
    int arr2[5] = {0}; // Are the subsequent four ints also zero?
}

And do those initializations mean something different in C and in C++? Does it depend on the version of the C standard?

Here are some fun ways to initialize a single int, most of which only arise in theory. This is perfectly legal code that compiles. Are they all zero, are only some zero, are they all possibly uninitialized? I don't know.

void blub() {
    int a4{};
    int a5 = {};
    int a6 = int{};
    int a7 = int();
    int a8{int()};
    int a9 = {int()};
    int* p1 = new int;
    int* p2 = new int{};
    int* p3 = new int();
}


The two cases I should look up for definitive clarification are a4{}; and a5 = {};. The others might also be enlightening eventually, but expressions such as int() come up rarely enough that you can deem those esoteric.

And it doesn't end with ints where you have a chance at initializing them explicitly with zero. Occasionally useful in real life: You have std::vector<int> and you want to enlarge it by calling resize(). Does it zero-initialize the new values?




I have dark memories of possible nonequivalence of the following. It might be a false memory from early learner days in 2006, but I've never clarified it. Assume X is a class with a custom default constructor, and no other constructors (in particular, X doesn't define a constructor that takes a std::initializer_list). Do all of these run the default constructor? Quick reality check with g++ --std=c++17 tells me: Yes, all of them run the default constructor. But does the language really guarantee it, ever since C++98? I assume so, then...

void blip() {
    X x1;
    X x2{};
    X x3 = {};
    X x4 = X();
    X x5 = X{};
    X* x6 = new X;
    X* x7 = new X();
}


I should really read the standard directly more.

-- Simon

Simon

#1
Templates in C++

Nice talks on Youtube:

CppCon 2016: Template Normal Programming, part 1/2
CppCon 2016: Template Normal Programming, part 2/2

By Arthur O'Dwyer. A thorough explanation of C++17 templates and their language rules. The examples are straightforward, and it avoids metaprogramming examples wherever possible to keep it accessible, hence the title "Template Normal Programming".

CppCon 2014: Modern Template Metaprogramming: A Compendium, part 1/2
CppCon 2014: Modern Template Metaprogramming: A Compendium, part 2/2

By Walter E. Brown. Many short examples of type manipulation. You'll see the implementation and ideas behind standard library utilities such as  std::is_same<T, U> or std::remove_const<T>. Introduction to SFINAE.

Functors

I should revisit one of my toy C++ examples: How nice of a functor can you create, e.g., with C++ template magic, to minimize boilerplate in the usercode. Here, I mean functor in the sense of category theory, not necessarily in the sense of object that overloads operator().

The use case is: You have

template <typename T>
struct ErrorOr {
    int error;
    T value;
};


... with the unenforced rule that we should use its good value if and only if the error is 0. You have a function func: T -> U. The goal is to lift func into the world of ErrorOr: The lifted function is of type ErrorOr<T> -> ErrorOr<U> and calls func when a good T comes, and merely copies the error code when an error code comes.

The solutions will likely not be beginner-friendly, and I'm sure that other people have already found solutions that make the usercode calls look reasonably straightforward. Nonetheless, I'll keep it in mind as an exercise for myself.

-- Simon

mobius

you may very well already be aware of this but I thought you might find it interesting; https://youtu.be/uTxRF5ag27A
interview with Bjarne Stroustrup, creator of C++. I found it quite interesting even though I as of yet know very little about C++.

Correct me if I'm wrong but memory tells me you told me years ago in some chat that you used a command line to compile your code exclusively? Is this still the case; was it with Lix? Why don't you use an IDE? If not; which do you use and why?
everything by me: https://www.lemmingsforums.net/index.php?topic=5982.msg96035#msg96035

"Not knowing how near the truth is, we seek it far away."
-Hakuin Ekaku

"I have seen a heap of trouble in my life, and most of it has never come to pass" - Mark Twain


Simon

#3
Quote from: mobius on June 07, 2023, 10:05:17 PM
you used a command line to compile your code exclusively?

Projects should be buildable from the command line. Then, if you prefer an IDE, you can import the project and build from the IDE if that's handy.

It's nice to tell people to install a compiler, possibly a build system too, and any required libraries. Then you can give them a one-liner to build. Especially on Linux, I can then then install everything from the command line in seconds (Linux distributions offer all the common dependencies), and have your project built and running in less than a minute. Ease of entry attracts codevelopers.

You can offer a project file in addition to your command-line instructions. If people want to download and install the big IDE, accept a proprietary license agreement if you chose a closed-source IDE, then wait until the IDE has started, then wait until the IDE has eaten all the project files, they can click compile and it's easy, too.

You can even satisfy both worlds with one set of build files. CMake is a popular build system in the C++ world, and several IDEs can read CMake files directly. If you choose CMake as your build system, your build files are good both for the command line and for IDEs. The downside: CMake is hard to learn. If you're a beginner, wait until your project is bigger than a few files before adding CMake support for your project. C++ gives you already enough strange things to wonder, don't add complexity before you need it.

The C++ tooling in general is annoying. Not only is the CMake language obtuse, but there is no common package manager. The out-of-box experience in other ecosystems is much better: D + dub, Rust + cargo, Python + pip, Javascript + npm, ..., Nonetheless, for gamedev, C++ is excellent. So is D, at the risk of less-trodden paths.

QuoteIs this still the case; was it with Lix? Why don't you use an IDE? If not; which do you use and why?

For C++ Lix in 2006 through 2011, I used Codeblocks on Windows, and the MinGW C++ compiler. I learned how to build from the command line first, in the most naive way: how to give all files to the compiler by hand. I learned Codeblocks only afterwards, to benefit from incremental compilation instead rebuilding everything when only one source file changes.

On Linux, I've never used IDEs, I do it all with the shell and a text editor. ack is a nice text-searching tool for the command line. Command-line git is available on practically every system in the world. In a way, the shell becomes an IDE with all these tools.

On Windows, the shell isn't as nice, and using IDEs should be more common.

-- Simon

Dullstar

On Windows specifically, the CLI is not super great. Useful for administrative tasks a lot, but in general it's a much bigger pain and a lot of the CLI tools target Linux (well, more accurately Unix). Even if Windows ports exist, you have to track them down yourself, while they probably just kind of end up on your Linux machine thanks to the package managers handling dependencies (plus a lot of them are typically bundled). GUIs are much more tightly integrated in Windows land (does Windows even allow terminal-only installations? I know there's a server edition, at least, but I'm pretty sure it has a GUI?), so solid CLI interfaces are less common. Of course, the plus side of this is that Windows GUIs seem to be more reliable.

For C++ specifically: Like Simon said, CMake is popular. There's an alternative called Premake that's easier to use, but also not nearly as well-supported. It's generally easier to set up from scratch, but if you're e.g. linking libraries it's less likely that the library will support it out of the box (but: if the library doesn't support either out of the box, it should be easier to make work). I haven't decided if I want to pick up the book Simon recommended for CMake, but I can confidently say I wouldn't recommend trying to learn it without a book. Pretty much every online resource I've found on CMake is either too basic (stops before your app would have reached the point where CMake is helpful) or too advanced (assumes too much existing CMake knowledge to be helpful for beginners).

For Windows/C++: Visual Studio seems more likely to be supported than Codeblocks. If you don't have a specific issue with Visual Studio being closed-source, that's the one I'd recommend learning.

Simon

#5
Dullstar Will Be Angry with Me

Task: From an array _queue of 19 different sound effects, take one sound effect near the front, play that sound, remove the sound from the array, shifting all following sounds towards the front by one slot. Re-insert the sound in the end slot. (Thus, the array will remain of length 19 and will still have all sound effects, only in a different order than before.)

Can you spot the bug?

{
    int i = uniform(0, 4);
// random index 0, 1, 2, or 3 into 19-element array
    Sound chosen;
    memcpy(chosen, _queue[i], sizeof(Sound));
    memmove(_queue + i, _queue + i + 1, sizeof(Sound) * (19 - i));
    _queue[19 - 1] = chosen;
    playSound(chosen);
}


You may assume it's C and _queue is of type Sound[19] with a typedef struct { ... } Sound;

(In Lix 0.10.12, the array length of 19 isn't hardcoded. I merely rewrote this for you to look like C where it's hard to concisely denote the array length.)

Hint

Documentation for memmove.

What does the code do for i = 0?

Solution

Memory bug: We read one element past the end of the array.

    memmove(_queue + i, _queue + i + 1, sizeof(Sound) * (_queue.size() - i));

Should become:

    memmove(_queue + i, _queue + i + 1, sizeof(Sound) * (_queue.size() - i - 1));

Example of the corrected behavior: Assume i = 2 and the _queue holds the 8 different elements 0 1 2 3 4 5 6 7. We want to move 3 4 5 6 7 (the hindmost _queue.size() - i - 1) one slot towards the front. See memmove().

The bug hasn't been a problem in practice, we never write out of bounds. But even reading out of bounds can theoretically hit forbidden memory and crash. Fix will immediately go into Lix 0.10.13.

Reminds me of how Dullstar and I reduced the chat crash in Lix 0.9.43 to a wrong strncpy() size computation. Shouldn't use strncpy() anyway, should usually use snprintf() instead.

Maybe I should have written the dumb loop myself that copies the sounds one-by-one. Harder to write the bug then, and it would immediately trigger D's bounds checking during debug mode.

-- Simon

Simon

#6
Last week, I wrote similar code in the day job. The task was to put the front element at the back, shifting every other element frontward by one slot. No randomness, we always move the front element. If we have 0, 1, 2, 3, 4, we want 1, 2, 3, 4, 0 afterwards.

    auto v = std::vector<X>{ /* ... */ };
    // ...
    assert (v.size() >= 2);
    const X oldFront = v[0];
    for (size_t i = 0; i < v.size() - 1; ++i) {
        v[i] = v[i + 1];
    }
    v[v.size() - 1] = oldFront;


This time, I didn't reach for memmove or std::copy or std::copy_backwards; I wrote the counting loop myself. But it's not as nice as I had hoped. The loop isn't necessarily more robust, it's more lines of code, and you still need to track oldFront separately.

It turns out that it's possible with an STL one-liner:

    #include <algorithm>
    std::rotate(v.begin(), v.begin() + 1, v.end());


The semantics of std::rotate is to swap the subranges [a, b[ with [b, c[ in place. Here: Take out the first element and stick it after all others.

You still need to test that the vector contains at least one element. Otherwise, v.begin() + 1 will step one past v.end(), and that will, at least on my private machine here, crash the program.

-- Simon