c++ unnamed programming

by on
3 minute read

There are only two hard problems in Computer Science: cache invalidation and naming things.
— Phil Karlton

I really don't like wasting brain cells needlessly making up names for stuff, but C++ is one of the most annoying programming languages on this subject that I use, at least in its current state (C++17). It lacks an identifier placeholder, such as _ from Haskell or Rust, while it provides idioms such as RAII, which at times couples perfectly with leaving things unnamed:

#include <mutex>
#include <thread>

int g_i = 0;
std::mutex g_i_mutex;

void safe_increment() {
    std::lock_guard<std::mutex> guard{g_i_mutex};
    ++g_i;
}

I'm obligated to give the lock_guard variable a name: guard or something. If removed (std::lock_guard<std::mutex>{g_i_mutex}) it spawns a temporary that just locks and unlocks before ++g_i. This is some trivial code, so in this sample it may be not be that annoying, but it can easily become in more complex code with multiple guards, that exist solely for the sake of their destruction.

Another situation where it may seem obligatory to name temporaries is with lvalue reference parameters:

ifstream file("file.dat", ios::binary);
vector<char> data(istreambuf_iterator<char>(file), {});

{} is quite powerful, notice it's avoiding the most vexing parse!

So, it's time to address this with black magic: materialized objects and comma operators!

First let's solve the last one. You may have already heard of std::move right? And probably knows that it's just a kind of cast operation, it doesn't move anything, it turns a lvalue into a xvalue (a kind of rvalue). Now you may have not heard of its cousin, which I just call lvalue:

template <typename T>
constexpr T &lvalue(T &&r) noexcept { return r; }

It just do the opposite of what std::move does. Actually, I rather preferred that std::xvalue and std::lvalue existed instead of std::move. Here's how we lure istreambuf_iterator's constructor:

vector<char> data(
    istreambuf_iterator<char>(lvalue(ifstream("file.dat", ios::binary))),
    {});

You may have not noticed but, this rvalue to lvalue conversion is quite a strange beast, check this:

int foo(int **pp) {
    return **pp;
}

foo(&lvalue(&lvalue(42)));

If we converted it to a lvalue, we can take an address right? And on, and on?

This is valid! It's called object materialization.

Now let's move on to address the first case:

#include <string>
#include <utility>
#include <iostream>

struct track {
    std::string msg;
    track(std::string msg) : msg(std::move(msg)) {
        std::cout << "begin: " << this->msg << std::endl;
    }
    ~track() {
        std::cout << "end: " << this->msg << std::endl;
    }
};

int main() {
    using namespace std;

    track("1"), track("2"), [] {
        cout << "hello" << endl;
    }();
}

Output:

begin: 1
begin: 2
hello
end: 2
end: 1

I've created this simple track class so you can follow construction and destruction order with me. All these tricks are taking advantage that temporaries created on an expression will live until end of full-expression. The interesting thing is that you can use the comma operator to join expressions that form a full one, it doesn't terminate a full-expression! Putting a lambda call at the end makes it look like a kind of with statement. How do we apply it to the previous safe_increment function?

void safe_increment() {
    std::lock_guard<std::mutex>{g_i_mutex}, ++g_i;
}

You may realize that the comma operator may be overloaded right? And that this could bring problems, for example if track class overloaded it. If you feel afraid regarding that when applying this idiom for some class you may circumvent the problem with:

void(track("1")), void(track("2")), [] {
    cout << "hello" << endl;
}();

So it won't matter whether track overloads comma or not.

That's all, I'll finish with some links on the matter:

Addendum post:

cpp
Spotted a mistake in this article? Why not suggest an edit!