Recently, during a technical meeting, a single line of code blew my mind. The talk was about a software library and how to use the C++ API. On one slide there was a statement of the sort
some_function(some_object) = some_value;
To experienced C++ developers, this might be nothing special. Even though I have completed larger projects in C++, I couldn’t remember having seen something like this before, not to mention having even used something like this.
As it turns out, such an assignment can be realized in (at least) two different ways.
- Returning an
lvalue
- Returning a proxy object with an overloaded assignment operator
So obviously the key lies in the method some_function
. Let’s have a look at two
examples illustrating the two points from above.
Returning an lvalue
Assume we want to write a method which gives access to the first element of a
vector
. Passing the array as a reference to the method and simply returning the first
entry doesn’t work. If you try
int first(vector<int>& array) {
return array[0];
}
you will get a error: lvalue required as left operand of assignment
message.
Ok, so what’s an lvalue
. To put it in simple terms, an lvalue
is something
that can appear on the left side of an assignment.
When we return the first element of the array as an integer, C++ stores the
integer as a temporary value—to be used in subsequent expressions. We can not
assign to such a temporary value, because it doesn’t have a proper in place in
memory. Usually, this value will only reside in CPU registers. The
returned integer is an rvalue
.1
We can modify the method such that it returns an lvalue
, by changing its
return type to int&
. A reference (instead of a temporary result) is an
lvalue
. We can assign to a reference without any problem.
The full example using a templated function then reads.
// compile with: g++ -o lvalue -std=c++11 lvalue.cxx
#include <stdio.h>
#include <vector>
using std::vector;
// The 'magic' lies in the return type. Returning a reference to the first
// element, makes it possible to 'assign to the return value' of the function.
template <class T>
T& first(vector<T>& array) {
return array[0];
}
// Use the accessor function in an example
int main() {
vector<int> my_array = {0}; // Create initial array with single entry
first(my_array) = 42; // Return lvalue and assign to it
// Print the content of the array
printf("The first entry of the array is: %d\n", my_array[0]);
return 0;
}
In fact, this shouldn’t be a surprise to me. I have used something returning lvalues
all
the time without even noticing. Writing my_array[0] = 42;
uses the overloaded bracket operator which–if
you check the implementation of vector in the standard template library–returns
a reference to the element.
Returning proxy object
An alternative way to achieve this is by returning an object whose operator=
method is overloaded. The implementation requires a bit more boilerplate code.
First, let’s create a templated class. The objects of the class should get a reference to an array during instantiation. A pointer to that object is stored. If you assign to the object, the overloaded assignment operator is called. Since we have more power using this alternative implementation, we will provide something more complex than a simple accessor for the first entry of the array. Instead, we will append the assigned value to the array. The class implementation looks as follows.
// compile with: g++ -o fancy_append -std=c++11 fancy_append.cxx
#include <stdio.h>
#include <vector>
using std::vector;
template <class T>
class Proxy {
// Proxy class which appends a value to the given array when used in an
// assignment.
public:
vector<T>* m_array;
// Constructor of the class, stores pointer to array
Proxy(vector<T>& array) {
m_array = &array;
}
// Push the new value to the internally stored array
void operator=(const T& value) {
m_array->push_back(value);
}
};
What’s left to do is to write a method that returns the proxy object. Since an
assignment to the method will append to the array, we call it fancy_append
.
// User-interface method, takes an array as argument. Values 'assigned to it'
// are appended to the array.
template <class T>
Proxy<T> fancy_append(vector<T>& array) {
return Proxy<T>(array);
}
int main() {
// Fill initial array
vector<int> primes = {2, 3, 5};
// Test fancy append
fancy_append(primes) = 7;
fancy_append(primes) = 11;
// Print all values in the array
for (auto const& i : primes) {
printf("Value: %3d\n", i);
}
return 0;
}
When you run the above code, you see that the array contains all primes up to 11.
Summary
Seeing a line of C++ code like some_function(some_object) = some_value;
,
shouldn’t have been such a surprise to me, since it was around me all the
time. However, if you have spent a lot of time in
Python-land, where such statements are not
possible,
this kind of code looks quite unusual at first sight. I think this tells us a
lot, how one gets used (not to say blind) to a programming language over time.
-
Compile something like
int a = first(my_array) + 9;
and have a look at the output withobjdump -d EXEC_FILE
. With my setup, this contains the assembler instructionadd $0x9,%eax
after the call tofirst()
. The rvalue was stored in the registereax
. ↩
This might also interest you