Construction junction, what's that function?

Construction junction, what's that function?

June 20, 2025

I saw something puzzling today while using zcov – a function I didn’t write that had no coverage. In my parsing code I have a small struct, something like this:

struct block {
    block();
    int id;
    int call_site;
    int nonlocal_return;
    std::vector<int> preds;
    std::vector<int> succs;
};

Coverage summary

The block constructor shows up twice in the report, and only one is covered. Where does this second constructor come from, and why is it not run? Let’s look at it in the zcov CFG visualization:

This is the covered function: block::block with 100% coverage

And this is the non-covered function: block::block with no coverage

Ok, so the covered one is the implemented block constructor, but what about the other one? C++ implicitly defines the copy- and move constructors for us, and since they are implicit they are not anchored to the struct definition. Defining (and defaulting them) would should change this.

struct block {
    block();
    block(const block&) = default;
    block(block&&) = default;
    int id;
    int call_site;
    int nonlocal_return;
    std::vector<int> preds;
    std::vector<int> succs;
};

Looking at the non-covered CFG again immediately shows us that the second constructor is the move constructor:

Block move constructor

There is still a few mysteries to solve. The coverage summary still only lists two constructors (the default and the move), but the copy constructor is defined. And why is the move constructor never called?

A piece of the puzzle is that the struct is defined in an unnamed namespace, so the compiler knows that it won’t be visible to the linker. No explicit copy of the struct is made anywhere, so it decides there is no need for the copy constructor. But what about the move? The struct is only used inside a vector, which does impose requirements depending on what operations are used. This is the relevant section since C++11 from cppref:

The requirements that are imposed on the elements depend on the actual operations performed on the container. Generally, it is required that element type is a complete type and meets the requirements of Erasable, but many member functions impose stricter requirements.

Since there are no struct block created outside the vector, I started looking for operations that would cause a copy or move. As it turns out, there is only one:

    fn->blocks.resize(nblocks);
    for (std::size_t i = 0; i != fn->blocks.size(); ++i)
        fn->blocks[i].id = getid();

Since we know ahead of time how many elements we need to construct we do it all in one go, and the blocks vector is only moved (pointer-swapped) after. GCC seems to not figure out that blocks is always empty before the resize call, which is why it has to emit the block move constructor, but since it never has to resize-and-move it is never called. We can confirm this by defining the copy and deleting the move constructor, and what do you know:

struct block {
    block();
    block(const block&) = default;
    block(block&&) = delete;
    int id;
    int call_site;
    int nonlocal_return;
    std::vector<int> preds;
    std::vector<int> succs;
};

CFG for block::block with no coverage

Finally, I tried adding the items with vector.push_back, and now both constructors had 100% coverage.

This was a fun puzzle. Using coverage this way actually gave a me couple of interesting insights. We can observe that the use of the vector was efficient, in the sense that each vector instance allocated exactly once. This is one of those cases where missing coverage is a good thing, and shows that coverage is a useful tool for program analysis beyond simple pass/fail on 80% coverage. We can also observe that GCC only uses the move if it can get away with it (or not at all, like in this case), which is nice since the non-move constructor can throw and is more expensive. This mystery also shows that the demangling in zcov needs some work, because it would be nice to quickly see exactly which overload the entry represents.