Void me

void On the plus side, it's a pretty funny thing. We can lead to void almost any type, create a pointer with the typevoid*which can address anything. We can also make a function with a return type void which returns nothing. Type Function Declaration void f(void) will just be a function with no arguments. But to have objects like void or write something like void& we can't. It's a little weird, but not enough to give you sleepless nights until you start getting weird bugs where void isn't void at all.

The problem arose where we didn’t expect it, namely, the benchmark framework was slightly updated on the project, did it seem like something like this could happen when running tests?

“Nothing good won’t happen,” said the technical lead, and on Friday evening he uploaded a new framework, bypassing these very tests. And he drove off to some conference.


“We’ve arrived, we’re stuck,” the QA team said on Monday, when none of the tests started. Hereinafter, gtl is a namespace for a custom implementation of std in the XXX engine, from the game templates library, almost completely interface compatible with std/eastl. And the code was something like this:

template <typename F, typename... Args>
auto test_running_time(pcstr test_name, F&& f, Args&&... args) {
    auto start = gtl::high_resolution_clock::now();
    auto result = gtl::invoke(gtl::forward<F>(f), gtl::forward<Args>(args)...);
    auto end = gtl::high_resolution_clock::now();
    
    gtl::duration<double> diff = end - start;
    test_printf("Test %s spent time %ds \n", test_name, diff.count());
    return result;
}

There are no, or rather there were no, mistakes here. The code does what we need, namely, it takes two time samples before and after calling the function, gets the difference in seconds, writes it somewhere in the internal logs and returns the result back to the calling function. And this worked for all cases until google benchmark was updated. After the update, the functions that return void — for them the compiler generates an error, indicating the variable declaration result.

траля-ля-ля, две простыни логов с ворнингами в шаблонах и вот она ошибка
...
error: 'void result' has incomplete type
     // тут еще много текста и наконец
     auto result = gtl::invoke(/* ... */)
          ^~~~~~

“Fuck you, what errors with the void,” you say and go to see what’s wrong. But the version of the framework just broke, i.e. updated, and now I have to live with it. First, let's try it head-on – just go through the tests and overload test_running_time() for those functions that return voidwe duplicate the entire body of the function, in the hope that the working day will end soon. But the problem is that we have more than 8k tests, and about ten percent of them are broken. This is annoying and after 10 replacements you realize that Sisyphus is rolling this stone somewhere in the wrong direction. And it would be okay with copy-paste and time, but imagine that you also have to protect reviews, and they really don’t like copy-paste, so in general this path is completely unsuccessful – duplicating each function template is dangerous for the mental health of your colleagues.

After thinking a little about the problem, you can come to another solution to extract the result type from the expression and immediately return it without intermediate storage.

template <typename F, typename... Args>
auto test_running_time(pcstr test_name, F&& f, Args&&... args){
    auto start = gtl::high_resolution_clock::now();
    SCOPE_EXIT{
      auto result = gtl::invoke(gtl::forward<F>(f), gtl::forward<Args>(args)...);
      auto end = gtl::high_resolution_clock::now();
      gtl::duration<float> diff = end - start;
      test_printf("Test %s spent time %ds \n", test_name, diff.count());
    }
    
    return gtl::invoke(gtl::forward<F>(f), gtl::forward<Args>(args)...);;
}

Oddly enough, it worked. But if you think about it, there are no secrets here, we simply used the destructor of a local variable, which will be called immediately after exiting the function, i.e. This does not disrupt the normal flow of test execution, it just looks a little strange, and at the same time it performs the function assigned to this piece of code – it measures the test execution time. The compiler doesn't mind that we return an object of type voidand although there is no object of type void, the compiler has a special rule for this case, which allows you to return void as the result of executing a function. Otherwise, half of the standard library would not work.

It almost worked, almost – because fifty dollars of tests (but that was already a victory, only five dozen, not eight hundred) still could not be instantiated by the compiler, and even the tech lead who returned from the conference threw up his hands. Having looked at this matter sadly, we commented out these tests so as not to stop the work of the QA team, telling them to fix this issue soon.

The sprint is over…

The remaining tests were not collected – this tormented the lead of the QA team, he tormented his wards with questions, and they, in turn, cried to the developers and promised to catch more bugs and trickier ones if these last tests were fixed.

So a couple of weeks passed, the QA became more and more irritable, and when going into the smoking room they looked expressively at the ax that stands in front of the door to the QA department, as a symbol of their irreplaceable work, apparently in order to motivate the pogromists to fulfill their promise.

As a result, my colleagues and I thought and made a type that would replace void and had similar functionality and which could be freely substituted where usual void couldn't cope. They called it simple boidand even if it means something a little different, its meaning (a ball, something small that can be returned from a function) is very suitable. An empty type that can be constructed from anything. It comes with two auxiliary wrappers to convert between each other where necessary. Where, the compiler was unable to return void – is now returningboid because it is a real object type that can be created normally.

struct boid {
    boid() = default;
    boid(boid const&) = default;
    boid(boid&&) = default;
    boid& operator=(boid const&) = default;
    boid& operator=(boid&&) = default;
    
    template <typename Arg, typename... Args,
        gtl::enable_if_t<!gtl::is_base_of_v<boid, gtl::decay_t<Arg>>, int> = 0>
    explicit boid(Arg&&, Args&&...) { }
};

template <typename T>
using wrap_boid_t = gtl::conditional_t<gtl::is_void_v<T>, boid, T>;

template <typename T>
using unwrap_boid_t = gtl::conditional_t<gtl::is_same_v<gtl::decay_t<T>, boid>, void, T>;

We rewrite the function code a little so that it can work with the new boid type, this will require two more wrappers so that the compiler can distinguish between these types.

template <typename F, typename ...Args,
    typename Result = gtl::invoke_result_t<F, Args...>,
    gtl::enable_if_t<!gtl::is_void_v<Result>, int> = 0>
Result invoke_boid(F&& f, Args&& ...args) {
    return gtl::invoke(gtl::forward<F>(f), gtl::forward<Args>(args)...);
}

template <typename F, typename ...Args,
    typename Result = gtl::invoke_result_t<F, Args...>,
    gtl::enable_if_t<gtl::is_void_v<Result>, int> = 0>
boid invoke_boid(F&& f, Args&& ...args) {
    gtl::invoke(gtl::forward<F>(f), gtl::forward<Args>(args)...);
    return Void();
}

An attentive habra reader may well have a question: why not simply change the return type in the remaining tests? The answer is simple – this was not always possible and the existing dependencies in the tests led to new changes, which led to other changes, and so on. So, armed with a new boid, all that remains is to rewrite the original example a little, and voila, the broken tests are back in action:

template <typename F, typename... Args>
auto test_running_time(pcstr test_name, F&& f, Args&&... args)
{
    auto start = gtl::high_resolution_clock::now();
    // gtl::invoke -> invoke_void
    auto result = invoke_boid(gtl::forward<F>(f), gtl::forward<Args>(args)...);
    auto end = gtl::high_resolution_clock::now();
    
    gtl::duration<double> diff = end - start;
    test_printf("Test %s spent time %ds \n", test_name, diff.count());
    return result;
}

Conclusion

All this abnormal programming, with the void overloading, raises the question – why?void Is that weird at all? The tech lead said, “Because C,” because it’s always easier to say that the old programming language is to blame than the newfangled standard. I don't know the history of the appearance of the type void in the language, but it would be interesting to know, maybe someone will write in the comments.

Thinkvoid is not an object type for a reason, but so that an instance cannot be passed void into a function, how then to process this argument? The question is certainly interesting.

void foo(int a, void b, float c);

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *