c++ unnamed programming
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:
- https://cplusplus.github.io/EWG/ewg-active.html#35
- https://groups.google.com/a/isocpp.org/d/msg/std-proposals/OKUpODP9-7w/sW5PmYZhCgAJ
- https://github.com/NicolBolas/Proposal-Ideas/blob/master/Unnamed%20Variables.md
Addendum post: