You want a type to store a JSON value: a simple one (like null, number, boolean, string) or a compound one (like a sequence/array or a dictionary/object). How hard can it be?
So you start with a std::variant
:
1
2
3
using Value = std::variant<std::monostate, int, double, bool, std::string,
std::vector<Value>, std::map<std::string, Value>>;
// fails here ^ with "'Value' was not declared in this scope"
This does not work. When encountering std::vector<Value>
on line 2, the
compiler will error with something like 'Value' was not declared in this
scope
, because Value
can only be used after the semicolon ;
at the end of
the line 2.
This is because using
is used in this context to create an alias (an
alternative name) to a type and the alias cannot be used (currently) to define
the very thing that it tries to name.
We can declare a struct
and have the variant as a member variable:
1
2
3
4
5
struct Value
{
std::variant<std::monostate, int, double, bool, std::string,
std::vector<Value>, std::map<std::string, Value>> data;
};
Unlike the using
above, this works. When the compiler encounters
std::vector<Value>
on line 4, the Value
type is incomplete, which allows
using it subject to limitations regarding the size of Value
. And it does not
matter because the size of data
depends on the size of std::vector<Value>
,
which is typically three pointers (for begin, end and capacity), it does not
depend on the size of Value
(similarly for the std::map
).
This is a very old trick, required to do basic things such as to define the
Node
in a linked list. The compiler can determine below the size of a Node
:
it’s the size of the member variable, which is a pointer:
1
2
3
4
struct Node {
Node * next;
// more members here
};
However the code below does not work, we hit the limitations of an incomplete
type, because to determine the size of a Node
the compiler needs to know the
size of the member x
, which is of the size of a Node
: the very thing that
it tries to establish to start with.
1
2
3
4
struct Node {
Node x;
// ^ error "field 'x' has incomplete type 'Node'"
};
The problem with this solution is that we have to keep on using .data
to
access the value we’re looking for inside a variable of type Value
. That is
not the end of the world, but we’ve been there before: overuse of c_str()
for
strings anyone?
This second attempt uses the advice from a stackoverflow answer using the Fix type:
1
2
3
4
5
6
7
8
9
10
template<typename T>
using Var = std::variant<std::monostate, int, double, bool, std::string,
std::vector<T>, std::map<std::string, T>>;
template <template<typename> typename K>
struct Fix : K<Fix<K>> {
using K<Fix>::K;
};
using Value = Fix<Var>;
There are three parts to the code above.
The first is straight forward. It declares Var
as a alias for a variant where
actual type to store in the std::vector
and std::map
is a type T
to be
specified later.
The last part just declares that our Value
type is a instantiation of Fix
with Var
as a template parameter.
But the middle part declaring Fix
is relatively unusual despite it’s brevity.
The stackoverflow answer provides a link to the wikipedia page describing the
fixed-point combinator. In summary, it’s about a higher-order function
which takes a function as an argument and returns a value that maps to itself
of its argument function (if such a value exists). The claim is that the Fix
template does such a thing at compile time, therefore the name “fix” does not
mean the verb “to repair”, but rather the verb “to fasten something in a
particular place”.
But I find it really hard to determine how the code maps to the mathematical
idea (we deal with types, not functions), so let’s look at the three elements
of Fix
.
First: it’s a template, the template parameter K
is a “template template
parameter”. Let’s remind ourselves. A template parameter is usually a type,
Also sometimes it can be something that’s not a type e.g. an integral value.
But there is a third choice, where the template parameter is a type which
itself it is a template: a “template template parameter”. That template that
K
is, takes one type as a template parameter.
Second: Fix
is derived from K<Fix<K>>
. Later, in using Value = Fix<Var>
,
Fix
ends up instantiated with K
being Var
, so this is an attempt to
derive from a Var
instantiated with the derived type. Deriving from a base
class that uses the derived class as a template argument is a known pattern
called curiously recurring template pattern, but in this case it’s a
more unusual usage because K
is a template template parameter.
Third: the constructors of the base class are lifted in the derived class. This
means that the derived class, which is meant to keep a JSON value, can be
constructed from an int
, std::string
, etc. in the same as a the base class
can, the base class being a std::variant
of those.
This attempt achieves the goal of not having to use .data
to reach the value.
This is because inheritance from std::variant
creates the situation where the
derived class is a variant compared with the previous attempt where the
class has a variant as a member variable.
But it does so with a significant cost of complexity. Sure, it’s one off complexity, which makes usage easier, but despite it’s brevity it’s still code that very few would be able to completely understand and explain how it works.
But what if from the previous attempt we try to keep the essential parts: the
inheritance mechanism and the using
to lift the constructors. Then we end
with something like:
1
2
3
4
5
6
7
template<typename T>
using Var = std::variant<std::monostate, int, double, bool, std::string,
std::vector<T>, std::map<std::string, T>>;
struct Value : Var<Value> {
using Var<Value>::Var;
};
This solves the problem of the first attempt, no need to use .data
, but with
much lower complexity than the second attempt, the troubling Fix
is gone,
we’re left with
using
statement for Var
(it makes sense, it’s used more than once
later)Var
instantiated with the derived typeusing
statement to lift the constructors of the base class into the
derived oneWe can use Value
like this:
1
2
3
4
5
6
7
int main() {
Value a{ 42 };
Value b{ "some string" };
Value v = std::vector<Value>{ a, b };
return std::get<int>(std::get<std::vector<Value>>(v)[0]);
// returns 42
}
Even the final solution is not trivial. But it actually demonstrates a strength of C++. We started with a very specific requirement and we could accomplish it, most importantly to simplify the usage which helps since most types are used more than once.
There are psychological obstacles in transitioning between the solution
attempts. The first attempt requires you to use .data
to access the value.
It’s easy to just stop there. The second attempt is very complex, it’s hard to
not be overwhelmed by the details of the complexity and continue to look for a
simpler solution.
Stack overflow answer using the Fix type
]]>In a way, what is the Pythagorean theorem but an algorithm to calculate the root of the sum of two squares?
But the things directly consequential for the way we program today have their origin in metamathematical questions about the limits of natural numbers systems, largely in the first half of the 20th century.
Kurt Gödel used a model involving primitive recursion to start with. It turns
out that a lot of mathematical functions on natural numbers are primitive
recursive, a scheme briefly described as: they are defined in terms of a
initial value, the successor function (i.e. plus_one), and recursively where
the value of a parameter is one less. So for example sum(a, b)
is defined in
terms of sum(a, 0) = a
and sum(a + 1, b) = sum(a, b) + 1
, similarly for
many other common functions like product, exponentiation, subtraction,
division, reminder etc. A generalisation of the rules of recursion results in
the ability to calculate any function that could be calculated (on natural
numbers).
Alonzo Church used lambda calculus for similar purposes, though there were initial debates of the equivalence. The Greek letter lambda is used to disambiguate between defining a function or calling it for particular arguments.
The debate was settled largely by Alan Turing who in turn started with the Turing machine model of a state machine that performs simple operations using symbols on an infinite tape.
Turing showed that the recursion, the lambda calculus and the Turing machine are equivalent: once a system is Turing complete, it can simulate a Turing machine (or equivalents) and can calculate whatever the other system can calculate as well.
Note that often in the proofs, for which these mathematical systems were developed, very very large numbers are involved. Mathematicians were aiming for easy to understand proofs and, as long as one can imagine the algorithm on how such numbers would be calculated, they were not concerned with actually calculating them in practice (e.g. how much paper would be required).
Programming languages are usually defined against a virtual machine.
Sometimes developers are clearly aware of this such as in the case of Java running against the Java Virtual Machine (JVM). Same applies to languages like C#.
C/C++ are also defined in terms of a virtual machine. Often developers are not aware of this because the C/C++ virtual machine is very similar to the physical machine: it has data memory (stack and heap), registers etc. This allows for good efficiency as only little runtime work has to be done to adapt the virtual one to the physical one.
These machines (the physical and the virtual ones) have their roots in the Turing machine, e.g. with the (finite) random access memory (and hard disks) as a substitute for the infinite tape.
Another major approach is the case where the virtual machine is a chain of
contexts where names are looked up. This has a long tradition that goes all the
way back to Lisp. Lisp embraced the lambda calculus paradigm in a dogmatic way:
everything is a lambda. Obviously functions are lambdas. Recursive function
calls are used to implement for
loops, the compiler tries to identify a
special type of recursion (tail recursion) and optimise it (to loop rather than
recurse). To implement lambdas properly, the Lisp compiler uses dictionaries to
hold the context of a function, it’s these dictionaries that are used to hold
data indirectly in Lisp, instead of providing direct support for data
structures.
Javascript inherited this approach from Lisp via Scheme (practically a Lisp subset). This is visible in the object creation in Javascript.
While programming environments do not have infinite resources, there are parallels to the Turing equivalence. Once a programming language has enough richness (which practically means most general usage ones), it can do any calculation that the other ones do. The difference is in efficiency.
Languages that use abstractions closer to the physical machine are more efficient at runtime (e.g. execution time, memory and CPU usage). It is possible sometime to compensate for an inefficient language by designing a solution to work around inefficiencies, e.g. in a reference counted garbage collected language, like C#, design the program to avoid memory allocations. Also inherent efficiency of the language can be negated by using an inferior algorithm.
Programming languages also differ in terms of the development effort required to solve certain types of problems. The IDE support, the available libraries, the number of concerns that the developer has to take care of, the ability of the language to scale in large code bases: they all make a difference on how long it takes to write and test a program.
There are lots of options and variations on this theme of “it’s all the same in terms of the calculation that is done, but a difference in the resources used (including time)”:
While is true that there are different equivalent computational models, inspired by mathematical research, which result in different programming languages choices, equivalence in mathematical sense does not make them entirely equivalent in efficiency (and there are many measures of efficiency).
E.g. doctrinary claiming that “everything is a lambda” is fine if you want to design a simple to understand mathematical model, but not fine if you want to ensure that you use a reasonable amount of memory and running time.
]]>Euclid’s Elements, the best book in maths, proposed this situation where propositions were proved to be true via deductive reasoning starting with axioms (and definitions).
As for the axioms, the belief was that they represent truths, why would one question it for “the whole is larger than the part”?
It turns out that most questioning was for the 5th postulate (axiom), because of its convoluted phrasing. The question was how many parallels exist in a plan through a point outside a straight line, and the 5th said that one precisely.
It took more than two thousands years to find that other options exist for the 5th, such that the option that there is more than one parallel (hyperbolic geometry), or the option that there is no parallel (elliptic geometry).
So then we moved around the 19th century from the situation where the geometry axioms were absolute truths, to one where they were just choices. They were not entirely arbitrary, because they work extremely well in certain human-scale situations (such a designing a building), but eventually break down at very small scale (such as when lines can’t be divided further when we reach atomic scales), or very large (such as cosmic) scales.
As a fun fact, when I introduced to my child the 2nd postulate, “a straight line can be made arbitrary longer at both ends”, they giggled and said “well, that’s only until it meets at the side of the Earth” and I explained how in Euclidean geometry a line would be tangent to the surface of the Earth, it’s not the 2D geometry on the surface of a sphere, but they should remember that thought.
They nodded to understand and took the 2nd postulate as a truth.
But eventually we had the discussion that elliptic geometries describe exactly the situation she thought of initially (the 2D geometry on the surface of a sphere) where straight lines do meet eventually and even such apparent self obvious truths that “a straight line can be made arbitrary longer at both ends” are questionable.
Around the middle of the 19th century to the first half of the 20th century mathematicians turned their attention to natural numbers due to their fundamental role and tried to put it on a clear axiomatic basis. Several systems emerged, some based on the Peano approach (start at zero and apply a successor function to generate the natural numbers), some based on set theory (the empty set is zero, the set containing the empty set is 1 and so on). Such systems share surprising properties such as the ability to express problems that cannot be proven.
But the question the is: are natural number real and the surprising properties unescapable or are they just one of the many possible human inventions just like geometry?
And the answer is that natural numbers are a human invention. The systems of natural numbers have many assumptions such as the ability to recognize entities as being unique or the same, and the ability to go on forever. When that is a good approximation for the problems we look at, using the natural numbers and their derivatives is very successful. But they fail for problems such as counting the grains of sand on a beach and establishing the length of the coast of Great Britain.
What’s not purely human invention are mechanisms that we share with other animals. The evolutionary basis of the natural numbers is the ability of animals to use the nervous system to for example recognise enemies: none (I’m safe), one (I might be able to deal with), two (I might need to run), three (I should run), a few (help), some, many, lots (no chance). While we have an innate basis to distinguish between 2 and 3 entities, we don’t have near the same confidence to distinguish without a lot of analysis between 246 and 250 (with reference to the 1987 movie “Rain Man”).
Long time ago I had to investigate why a list in a product has lots of entries. I picked one such case. It had about 2000 entries. That’s a lot, but decided to go through them and classify them to try figure out what’s the underlying cause. Are certain types of entries more common? What’s the cause? In the process of going through the 2000 entries one by one I soon discovered another issue.
The list displayed the entries page by page, but pagination was broken and the first 50 entries were repeated instead of displaying 50 different entries each time. So in my case, the first 50 entries would have been repeated about 40 times.
And this issue was not reported. Thousands of customers complained by the large number of entries, but none complained that only 50 are displayed, the rest are repeats.
My empirical observation is that somewhere between 20 and 50 our brains stop having a intuitive perception of precise numbers and use heuristics based on “there’s lots”, rather than precise numbers.
]]>I learned the Aesop fable when I was little, it was funny, the hare was silly (why did it stop running?), but did not think that’s something that can be applied, it was just a funny children story.
Many years later I was cycling from London to Brighton. Somewhere, midway the distance, I was going slowly when a couple of cyclists gently overtook me. I thought “I can keep up with them”, and I pedalled harder to keep up with them. Half an hour later, also a bit tired from the extra effort to keep up, I started to have doubts that I was going in the right direction and eventually I stopped to check the map. It turned out that instead of heading southwards, at some point after a gentle curve we ended going eastwards, they were not going south to Brighton, I cycled fast for most of the half an hour in the wrong direction. I would have made more progress with less effort if I would have cycled instead in the right direction.
Accidental design is the situation where there something was done, it had bugs, bugs were fixed by adding complexity, and that repeated, until the code is really complex and still not bug free, but at no point anyone stopped to ask “Are we doing the right thing? Is there a simpler option available?” and pursue the simpler option earlier on.
If you have a vision for the direction of the software product and implement things so that fit within that vision and move in the right direction, or having an idea of which bug fixes would have the largest impact and fixing those bugs, will lead to more results than just doing random stuff quickly or fixing quickly insignificant bugs. Working hard might make less progress that working clever.
Corollary: it’s not enough to be clever, it’s also important to be wise
The essence of agile is figuring out what’s important, work on that, be intellectually alert on what’s happening, think, don’t be afraid to change plan when it makes sense based on new information. It does not mean avoiding all planning and design.
Lengthy and costly “agile” development would often have benefited by a little bit of design.
There is the canonical example of the best method to fill, seal and attach a stamp to a number of envelopes. There is the option of filling all envelopes, seal all the envelopes then stamp all the envelopes or the other option of doing one envelope at a time (fill, seal, stamp, then move to the next envelope). One of the methods is faster, and if there are enough envelopes to handle, a little bit of testing, to see which one, will give surprisingly faster outcomes.
Sometimes it is possible to get a good estimate on how long will it take to develop some functionality: that’s the case when the team has the experience of similar previous work.
But doing repeatedly the same thing becomes low value, there is more value in doing new things that have not been done (the same way) before, That’s harder to estimate to the same degree of precision.
There is no point pretending that both are the same, in particular trying to commit to precise timelines when there is a lot of uncertainty: that’s either naive or unethical.
A typical antipattern is as follows (PM stands e.g. for a project manager, Dev stands for a developer):
PM: How long will it take? Dev: I don't know. PM: No. How long will it take? Dev: I really don't know. PM: No, no, it does not work like that, you have to tell me how long it will take, Dev: I don't know. 5 days? PM: Great, I'll write that down. Later that day Stakeholder: How long will it take? PM: 5 days. 5 days later PM: Is it done? Dev: No, because I hit an unexpected problem ... PM: Fine. How long will it take? Dev: I don't know. PM: No, no, you have to tell me how long it will take. Dev: I don't know? 3 more days? Later that day Stakeholder: What's the project status? PM: It's late by 3 days. It's the developer's fault. Stakeholder: Tell them to not be late again.
What happens above is that the developer’s uncertainties are ignored. The problem was framed wrongly, on the assumption that precise estimates are possible, from the interaction above why would any reasonable person really believe that is the case? A more grown up behaviour would have been to at least acknowledge the uncertainty, try to explore what would reduce it e.g “what information is needed to reduce the uncertainty?”, “would a more experienced developer be able to provide better estimates?” and monitor progress, don’t wait to the end of the 5 days, maybe a solution can be found to overcome the unexpected difficulties before the deadline.
What also happened is that the project manager defines their role as of just relaying data between the developer and stakeholder not really doing their job, the stakeholder let them get away with that and suffers unpleasant surprises, while the developer (who has no project management expertise) suffers unconstructive pressure from those two and gets little credit for the technical achievements.
This gets us to the point that a lot of the software development is not about technical issues, but about interacting with other people. It’s so satisfying and beneficial to resolve problems based on reason and facts. But to get there requires a lot of human interaction skill.
Often in meetings there is a question and the answer is “Blah, blah, blah, blah”, maybe true and maybe interesting, but not really answering the question.
There are at least two behaviours that help to get back to reason and facts. One is to do active listening:
The second, related, behaviour is to respect people. Generally people want to achieve things, to be recognized for that, even when we malfunction in corporate groups, stay positive and focus on what would make things better.
]]>When you have an int
variable, const
can be used before or after the type,
it’s just a matter of taste, it does not make a difference to the type of the
variable. You can’t change the value for a const int
, but you can take a
(non const copy and change the copy as much as you want to:
1
2
3
4
5
6
7
8
9
10
const int some_int = 41;
// equivalent to:
// int const some_int = 41;
// would get an error:
// some_int = 42;
int another_int = some_int;
another_int = 42;
When you have a pointer however, it does matter if the const
is before or
after the *
, you can have both or either. The const
preceding *
refers to
the pointed type: can’t change the pointed value. The const
, following *
refers to the value of the pointer: can’t change the pointer itself, but can
take a (non const) copy, and change the copy as much as we want to (to point to
something else).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const int * const some_ptr = &some_int;
// equivalent to:
// int const * const another_ptr = &some_int;
// pointer to const int, pointed value is const
const int * ptr_to_const = some_ptr;
// can't do this:
// *ptr_to_const = 42;
// or this
// *some_ptr = 42;
// but can do this:
ptr_to_const = nullptr;
// pointer to int, pointer is const
int * const ptr_const = &another_int;
// can't do this:
// ptr_const = nullptr;
// or this:
// some_ptr = null_ptr;
// but can do this:
*ptr_const = 42;
std::vector
typically stores three pointers to manage the start , the used
and the available space of the dynamically allocated array.
Despite these pointers, when you put const
in front of a vector, it behaves
like an int
: can’t change the values in the vector. When you take a copy of
the vector it copies all the values of the vector, not just the three pointers.
When you compare two vectors, it compares the values, not the three pointers.
One shortcut is to say that vector propagates const
deep, does a deep copy,
implements deep comparison.
The deep const
behaviour is why we use way more often const
std::vector<int>
than std::vector<const int>
.
std::shared_ptr
however has pointer semantics: const std::shared_ptr<int>
means can’t change this pointer, but can change the pointed value, copy points
to the same thing as the original rather than copy the pointed value,
comparison compares the underlying pointer.
We say that std::shared_ptr
uses shallow `const, shallow copy and shallow
comparison.
std::string_view
and std::span
provide views into contiguous sequences. To
do that they each typically store two pointers.
They copy shallow (like pointers), but then they diverge.
std::string_view
offers a immutable view, so const std::string_view
, or not
const
, the view is constant, to say that constness propagates deep is somehow
incorrect: it’s always deep const. std::span
offers a mutable view, so
const std::span<int>
is shallow i.e. can’t change the pointers, but can
change the pointed values; use std::span<const int>
to make the view
constant.
Comparison operations of std::string_view
perform a deep comparison,
comparing the pointed sequence. std::span
does not implement comparisons.
Using terminology like deep or shallow is only useful as a quick descriptive word, but it does not offer any clue into why the examples above were designed that way and what is good choice for a new type. A better insight is given by the thought that good types behave properly. One reasonable expectation is the following:
Axiom: If you take a copy and change the original, then the copy is different from the original
This is only one of the many expectations of a regular concept in particular. Even if you design a class that’s not regular, it’s often better to not implement behaviour that would lead to confusing behavior (e.g. not implement copy, move, comparisons) than to implement it and contradict axioms like the one above.
When we look at it this way, it makes sense that std::vector
does deep copy,
const
and comparisons: it’s like an int. It makes sense that
std::shared_ptr
does it shallow: it’s like a pointer. std::string_view
gets
away with a combination of shallow copy and deep compare because it’s a
constant view: the deep compare is debatable, but it’s handy as well.
std::span
does not get away with a combination of shallow and deep because
it’s not a constant view; a combination of shallow copy, shallow const
and
deep compare would not meet the axiom above.
begin()
s are the same.
The vector has a member method begin()
that returns an iterator, The iterator
wraps a pointer, the pointer points to the first value in the underlying array
(assuming the vector is not empty). That iterator can be used to point to other
values in the underlying array and get a reference to read or write those
values.
What if the vector is const
? The design decision for std::vector
was to
make it a regular type, hence it should behave like int
, hence a const
std::vector
should not allow you to change it’s values. That’s why we have
std::vector<int>
and const std::vector<int>
, but rarely std::vector<const
int>
.
Therefore the vector has two begin()
member methods:
1
2
3
4
5
6
7
8
9
10
11
template<typename T>
class vector
{
// lots of other details skipped ...
constexpr iterator begin() noexcept {
return iterator(begin_);
}
constexpr const_iterator begin() const noexcept {
return const_iterator(begin_);
}
};
For a const
vector, the additional begin() const
member method will be
selected by the compiler. That begin()
returns a const_iterator
, which
gives a reference to the const
value, therefore through the const_iterator
we can read the value, but not modify it.
Later the vector got a cbegin()
member method:
1
2
3
4
5
6
7
8
template<typename T>
class vector
{
// lots of other details skipped ...
constexpr const_iterator cbegin() const noexcept {
return begin();
}
};
The intent for that member method is to obtain a const_iterator
when you have
a non-const vector.
A similar situation is for the end of the sequence with two member methods for
end()
and also a cend()
member method.
From the point of the user one can mix and match comparing iterator
s with
const_iterator
s, which is handy, but results in a large number of functions
to write by the implementer of the container (albeit all these functions have
simple implementations).
std::string
is a much older type than std::vector
. From that older history
it has member methods like find
that return positions, with a special value
npos
for “not found”.
When large parts of STL such as vector
where added to the standard library,
std::string
was retrofitted to have member methods like begin
and cbegin
just like the vector
.
The array however does not have member functions. So then standalone template
functions were added for begin
, end
, size
. The syntax through which they
take the array is a bit surprising at first, but other than that the
implementation is straight forward:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<class T, std::size_t N>
T * begin(T (&array)[N]) {
return array;
}
template<class T, std::size_t N>
T * end(T (&array)[N]) {
return array + N;
}
template<class T, std::size_t N>
size_t size(T (&array)[N]) {
return N;
}
And overloads were created to call the member functions e.g. begin()
otherwise, so that you can use the free function std::begin
with both arrays
or containers.
With that we can loop:
1
2
3
4
5
for (std::vector<int>::iterator it = vec.begin();
it != vec.end();
++it) {
// dereference it to either read or change values
}
The whole std::vector<int>::iterator is a mouthfull, especially when using
cbegin`:
1
2
3
4
5
for (std::vector<int>::const_iterator it = vec.cbegin();
it != vec.cend();
++it) {
// dereference it to read values, but can't change values
}
auto
can simplify such loops:
1
2
3
for (auto it = vec.begin(); it != vec.end(); ++it) {
// dereference it to either read or change values
}
And range-based for loops were introduced to simplify theese common scenarios further:
1
2
3
4
5
6
7
for (const auto & value : vec) {
// read value
}
// or if needed
for (auto & value : vec) {
// read or change value
}
The range-based for loops rely on the free functions std::begin
and
std::end
.
std::filesystem::directory_iterator
is called an “iterator”, and yet it can
be used in a range-based for loop to iterate through files in a folder as
below:
1
2
3
for (const auto & dir_entry : directory_iterator("C:\\Some folder")) {
std::cout << dir_entry.path() << '\n';
}
The reason it gives access to a directory entry rather than just the path is
that the OS APIs it uses, e.g. FindFirstFileExW
and FindNextFileW
in
Windows, also return file attribute, not just the file names.
To do so it has member functions begin()
and end()
which each return …
drum rolls … a directory_iterator
. begin()
just returns a copy, while the
end()
returns a default constructed one. This ends up roughly equivalent to:
1
2
3
4
5
for (auto it = directory_iterator("C:\\Some folder");
it != directory_iterator();
++it) {
std::cout << it->path() << '\n';
}
The directory_iterator
is a weird type: it started life as something close to
an iterator, then become more like a range, while it preserved the old name.
The design/documentation also forces the storage of the handle returned from
e.g. FindFirstFileExW
in a heap allocated, reference counted location. Also
it’s end iterator predates ideas of a sentinel for determining if we reached
the end.
Dereferencing the directory_iterator
gives access to a const
std::filesystem::directory_entry
, so in a way it’s a const iterator, but while
it has a begin()
, it does not currently have a cbegin()
.
A string_view
gives read only access to a memory contiguous range of
characters. That allows some unification for functions that take either string
literals or std::string
(and do not care about zero
termination), so the access has to be read only to accomodate
literals.
As for the range, it typically stores two pointers and returns the first as the
begin()
and the last as the end()
. But the iterators it returns are const
iterators, and it’s cbegin()
returns the same const iterator.
But what if you want access to a memory contiguous range of values, not
necessarily characters (so not interoperability with std::string
is required)
and also get write access, not just read?
Enter std::span
. It derives from a class gsl::span
, where gsl
was a
largely Microsoft supported library intended to make things safer.
Like string_view
, to expose the range, span
typically stores two pointers
and returns the first as the begin()
and the last as the end()
. But the
iterators it returns are NOT const iterators.
And here is where things get an interesting turn. gsl::span
used to have a
member function cbegin()
that returned a const iterator. In the process of
standardising the type as std::span
people noticed that std::span::cbegin()
returns a const iterator, but the free standing function std::cbegin
returns
a mutable iterator (i.e. different types, see
PL247). That’s because the
free standing std::cbegin
just makes the object constant and calls begin()
on it, hoping to select the const member function begin()
.
But std::span
has issues with constness. As a type it makes sense to model a
pointer where just because the pointer is const
(shallow), does not mean that
the pointed data is const
(deep). It’s member function begin()
returns a
non const iterator (allowing to change the pointed data) even if the span
object is const
(can’t change the pointers it embeds).
In the rush of standardising std::span
, it’s cbegin()
member function was
dropped, see LGW3320, though
hopefully these issues will be sorted out (see proposals
P2278r4
and
P2276r1).
std::generator
is a template type introduced in C++23 to support one of the
usage scenarios of C++ coroutines: generate sequences.
1
2
3
4
5
6
7
8
9
10
11
12
std::generator<int> natural_numbers() {
int crt = 0;
for(;; ++crt) {
co_yield crt;
}
}
void foo() {
for (const int x : natural_numbers() | std::views::take(10)) {
std::cout << x << ' ';
}
}
For the above to work, the std::generator
implements begin()
and end()
,
but: “It is an undefined behavior to call begin()
more than once on the same
generator
object.”
I suspect this is because like directory_iterator
manages a resource (the
HANDLE
returned by FindFirstFileExW
), the generator
also manages a
resource (the coroutine state). The directory_iterator
made the choice of
using reference counting to manage this resource, making it copyable at
additional cost, while generator
gives up iterator copy and convenience in
order to avoid that cost.
This article is part of a series on the history of regular data type in C++.
For the end of this series I’d like to make the case that the regular concept is part of a larger idea that traverses the “Elements of Programming”. And that is that it’s things that we know that we can use for algorithmic efficiency to reorganize calculations and eventually skip unnecessary calculations.
I’m going to try to illustrate this using some memorable canonical examples.
1
2
3
4
// domain of the function are x and y such that `x + y` does not overflow
unsigned int add(unsigned int x, unsigned int y) {
return x + y;
}
In this case the precondition could be checked using this predicated, but the
calculation is about as costly as the work inside add
itself
1
2
3
bool in_domain_of_add(unsigned int x, unsigned int y) {
return ((UINT_MAX - x) >= y);
}
This is an example where a check could be done, with some loss of efficiency, but also some efficiency gained if skipped when it is known that overflow will not happen for those specific input values.
There is a family of algorithms that use divide and conquer on a already
ordered range: partition_point
(illustrated above), lower_bound
(partition_point
for less than a given value), binary_search
(lower_bound
plus equality check).
Below is for example binary_search
that can efficiently find in the value
is in the range by halving the search range at each step leading to O(lg(N))
time complexity.
1
2
3
template<class It, class T>
// It is random access iterator
bool binary_search(It first, It last, const T & value);
But binary_search
itself it does not check if the sequence is sorted. Similar
issues apply to lower bound and partition point for the requirement that the
sequence is partitioned for the comparison used (defaults to ‘<’).
A runtime check that the input range is sorted would degrade the time
complexity to O(N)
.
This is an example where a check could be done, but at a meaningful performance cost.
A lot of the algorithms operate on ranges of data that are often expressed as
two iterators as generalized pointers, as in the case of binary_search
. They
take first
and last
then for linear traversal there is a loop like:
1
2
3
while(; first != last ; ++first) {
// use value pointed by first
}
But it does not check that last
is reachable from first
. The caller needs
to know that. Such a check is often not even possible for certain data
structures.
The algorithm rotate takes three iterators:
1
2
3
template<class It, class T>
// It is random access iterator
It rotate(It first, It middle, It last);
The result is that between first
and last
we end up with the values that
were between middle
and last
, followed by the values that were between
first
and middle
. It does that in a surprisingly O(N)
time complexity
with constant memory requirements.
Roughly the code idea to make that work is that it figures out where the value
at first
needs to go. It saves the target value in a temporary and stores the
value from first
. Then it figures out where that temporary needs to go and
so on until we complete a cycle going back all the way to first
. And then
there are GCD(M, N)
such cycles where N
is the total number of values between
first
and last
, M
is the number of values between first
and middle
and GCD
is the greatest common divisor.
This is interesting because, in the C++ world of destructing moves, every time
we move from a value, the move (constructor or assignment) also changes the
moved from object. This is not required in the case of rotate
because “we
know” based on the clever reasoning that each of the GCD(M, N)
number of
cycles will be complete. Currently this might be an optimisation that does not
take place in C++ because the compiler does not know that.
The replace traverses a linear sequence and replaces a value with another:
1
2
3
template<class It, class T>
// It is forward iterator
It replace(It first, It last, const T & old_value, const T & new_value);
But what if we use like this with references to values from the sequence?
1
std::replace(v.begin(), v.end(), v[3], v[2]);
If the input is: 0, 2, 1, 2, 3, 2, 1
and expect to replace 2
with 1
and
expect to get 0, 1, 1, 1, 3, 1, 1
you might be surprised when you get 0, 1,
1, 1, 3, 2, 1
.
Although const
, the problem is that the old_value
is alised, it’s also part
of the sequence, and part through the replacement we change what we’re trying
to do. Another way to look at aliasing is that v[3]
is accessed both via the
pointer behind the const T &
and the pointer behind the It first
after it
gets incremented a few times.
In correct usage we know we’re not doing what the example above does, but it’s
an interesting fact that in many cases (correct, unlike the incorrect usage of
replace
above), the compiler does not know that we’re not aliasing and
inserts more code to cope with that.
Note that one of the reason Fortran is faster than C/C++ is that Fortran assumes that function arguments do not alias.
Naive implementations of Fibonacci sequences take exponential time with the
argument, so they get slow very fast taking seconds for an argument of 40
something. A slightly better option is linear in time. But efficient
implementations can calculate it in logarithmic time taking less
than a second for an argument of 1 million. The idea is that to
calculate the values for N
and N - 1
, one multiplies a matrix with the
values at N - 1
and N - 2
. Repeat the process and you end with multiplying
the matrix with itself. We know that the matrix multiplication is associative
and that’s what enables it to be done in O(lg(N))
time.
Note that’s covered in chapter 3 of EOP.
When you apply a function T f(T x)
repeatedly the outcome can be:
unsigned int
increment
terminates at the max value if we don’t want it to overflowusigned int
increment
that goes back to 0
after the max value7, 2, 0, 1 , 2, 0, 1, ...
For this domain if the shape is not infinite one can use algorithms that determine the shape by having a fast traversal followed by a slow traversal and testing for end (terminating) or the two traversal giving the same value (circular or ρ-shaped).
Here is the core idea:
1
2
3
4
5
6
7
auto slow = x;
auto fast = f(x);
while (fast != slow) {
slow = f(slow);
fast = f(fast);
fast = f(fast);
}
These obit algorithms are quite obscure, but they show in the simplest setting the reliance of algorithms on regularity (in this case it’s regularity that ensures that the slow traversal follows the fast one).
Note that’s covered in chapter 2 of EOP, but regularity is fundamental for most algorithms.
You have a vector of unique file names corresponding to files that you want to update. You want to separate this into the file names that correspond to files that already exist and the ones that are missing. Then further separate the existing file names into the ones that are the correct version vs. older/different version. That results in: files names that correspond to files that are OK, need updating or need installing for the first time.
We could use the partition
algorithm twice where the predicate checks if the
file exist and if the file version is correct.
1
2
3
template<class It, class UnaryPredicate>
// It is bidirectional iterator
It partition(It first, It last, UnaryPredicate p);
But the problem is that the predicate is not a regular function. EOP and the
C++ standard define a predicate as a regular function. But in the case of the
partition
the predicate is meant to be called only once for each value in a
sequence of unique values. Regularity for functions involves calling repeatedly
a function with equal values. Well, that’s not the case here.
This illustrates that for library/language design there is a trade-off between elegance/simplicity and functionality/flexibility.
If you insist that the partition
predicate has to be a regular function,
you end up with a more elegant/simpler solution at the const of the kind of
problems that the algorithm can solve. The example I gave is probably
dominated by IO operations, but should you aim to solve if efficiently, one
solution involves exactly the same code as the partition
algorithm.
So if values are good and aliasing is so problematic, why don’t we just use
values? Why do we pass vectors by reference, e.g. auto foo(const
std::vector<T> & vec)
? The aliasing problems come from multiple pointers to
the same data, where the pointers are either embedded into iterators or through
the usage of references.
I guess that partly the answer is in EOP chapter 2.5. After describing orbits in terms of transformations in most of chapter 2, at the end of chapter it describes the duality between actions and transformations: they can be each described in terms of the other:
1
2
3
4
5
6
7
8
9
10
// action defined in terms of transformation
void a(T & x) {
x = f(x);
}
// transformation defined in terms of action
T f(T x) {
a(x);
return x;
}
Mathematically they are equivalent, the difference is in efficiency: if T
is
a large object and the change is small, actions could be much faster. When
passing a large object and there is no change we use const
reference.
Fundamentally it comes to: when we know that the data is not aliased and the data is large, transferring data by just transferring a pointer to the data is faster than transferring a copy of the data.
Alex Stepanov and Paul McJones: Elements of Programming aka. EOP
Value Semantics: Safety, Independence, Projection, & Future of Programming -
Dave Abrahams CppCon 22 for the
std::replace
example
This article is part of a series on the history of regular data type in C++.
Around 2018 I showed how to do it using tie_members
and
macros:
1
2
3
4
5
6
7
8
9
10
11
struct person {
std::string first_name;
std::string last_name;
int age{};
};
inline auto tie_members(const person & x) noexcept {
return tie_with_check<person>(x.first_name, x.last_name, x.age);
}
MAKE_STRICT_TOTALY_ORDERED(person)
Then in this series of articles I showed hot to do it using the spaceship operator:
1
2
3
4
5
6
7
8
struct person {
std::string first_name;
std::string last_name;
int age{};
constexpr std::strong_ordering
operator<=>(const person &) const noexcept = default;
};
Using the spaceship operator is an improvement, but still has issues.
Where I would like to get is something like:
1
2
3
4
5
regular_struct person {
std::string first_name;
std::string last_name;
int age;
};
The definition for the regular_struct
(potentially user defined) should
create the regular boileplate: default constructor (including defaulting the
int
to 0
, copy, move, comparisons, etc.