What is wrong with std :: visit in modern C ++

Original author: Matt Kline
  • Transfer

Sigma type and you


Let's talk about a simple but powerful programming concept — sigma types.

A sigma type (sum-type, labeled combination) may contain values ​​of one and only one of several types. For example, consider the settings in the INI -like configuration file. Let each setting be a string, an integer, or a boolean value. If we wanted to make our own library in C ++, we would write something like this:

structSetting {union {
        string str;
        int num;
        bool b;
    };
    enum Type { Str, Int, Bool };
    Type tag;
};
// Отображение настроек на их именаusing Settings = unordered_map<string, Setting>;

This is a dangerous path, for you always have to remember a few things:

  • Update tag when assigning a new value.
  • Read from the union only the correct type in accordance with the tag.
  • In time to call constructors and destructors for all non-trivial types. In this example, this is only a string, but there could be more.

If you forget at least one of these points, the object will be in an incorrect state, and there will be weeping and gnashing of teeth. All this magic can be encapsulated and work with style through a set of methods such as getType(), asBool(), asString()which also look bulky. Moreover, such a solution only shifts the problem to the one who will implement these methods. He will still have to maintain invariants without any help from the language.

It would be much better if the general purpose sigma type was in the standard library. In C ++ 17, we finally got it! It is called std :: variant , and now we will get to know it better.

Using std::variant


variantIs a class template that accepts types that it can contain as template parameters. Instead of the code from the example above, we could define the type of configuration as variant<string, int, bool>. Assigning the value to variantworks is quite expected:

variant<string, int, bool> mySetting = string("Hello!"); // или
mySetting = 42; // или
mySetting = false;

Putting the value in variant, we will one day want to extract it, and more importantly, find out its type. This is where the fun begins. Some languages ​​provide special pattern matching syntax like this:

match (theSetting) {
    Setting::Str(s) =>
        println!("A string: {}", s),
    Setting::Int(n) =>
        println!("An integer: {}", n),
    Setting::Bool(b) =>
        println!("A boolean: {}", b),
};

but this is not about C ++ 17 ( * ). Instead, we were given a helper function std::visit. It accepts variant, which needs to be processed, and the visitor object , which is called for any type in the transmitted one variant.

How to define a visitor? One way is to create an object in which the call operator is overloaded for all the necessary types:

structSettingVisitor {voidoperator()(conststring& s)const{
        printf("A string: %s\n", s.c_str());
    }
    voidoperator()(constint n)const{
        printf("An integer: %d\n", n);
    }
    voidoperator()(constbool b)const{
        printf("A boolean: %d\n", b);
    }
};

It looks terribly verbose, and it becomes even worse if we need to capture or change some external state. Hmm ... lambdas are great at capturing state. Can you make them a visitor?

make_visitor(
    [&](conststring& s) {
        printf("string: %s\n", s.c_str());
        // ...
    },
    [&](constint d) {
        printf("integer: %d\n", d);
        // ...
    },
    [&](constbool b) {
        printf("bool: %d\n", b);
        // ...
    }
)

Already better, but in the standard library there is nothing like the function make_visitorthat would make the called object from several lambdas. Let's write it yourself.

template <class... Fs>
structoverload;template <classF0, class... Frest>
structoverload<F0, Frest...> : F0, overload<Frest...>
{
    overload(F0 f0, Frest... rest) : F0(f0), overload<Frest...>(rest...) {}
    using F0::operator();
    using overload<Frest...>::operator();
};
template <classF0>
structoverload<F0> : F0
{
    overload(F0 f0) : F0(f0) {}
    using F0::operator();
};
template <class... Fs>
automake_visitor(Fs... fs)
{return overload<Fs...>(fs...);
}

Here we used templates with a variable number of parameters that appeared in C ++ 11. They need to be defined recursively, so we first declare the recursion base F0, and then declare a set of constructors, each of which bites off one element from the template parameter list and adds it to the type as a call operator.

It looks troublesome, but do not despair! C ++ 17 introduces a new syntax that will reduce all this code to

template<class... Ts> structoverloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;

Simple, isn't it? If you don’t like any of these options, use the conditional compile-time operator (if constexpr) from the C ++ 17 standard:

[](auto& arg) {
    using T = std::decay_t<decltype(arg)>;
    ifconstexpr(std::is_same_v<T, string>){
        printf("string: %s\n", arg.c_str());
        // ...
    }
    elseifconstexpr (std::is_same_v<T, int>) {
        printf("integer: %d\n", arg);
        // ...
    }
    elseifconstexpr (std::is_same_v<T, bool>) {
        printf("bool: %d\n", arg);
        // ...
    }
}

Looks better?

Not


All this bother for std::visitis sheer insanity. We started with a simple goal: look at the contents of the sigma type. And to complete this modest mission, we had to:

  1. Identify the callee that includes a lot of uniform code, or
  2. Define behavior using lambdas, which required:
    • Understanding patterns with a variable number of parameters in their entirety of their recursive magnificence, or
    • Close acquaintance with the- usingdeclarations with a variable number of parameters, which only appeared in the standard C ++ 17.

    or
  3. Use branching compile time, which requires knowledge and a deep understanding of the syntax constexpr if, as well as all sorts of interesting things type_traits, like std::decay.

None of these concepts is a mystery to an experienced C ++ developer, but these are fairly “advanced” features of the language. Something obviously went wrong if you need so much to know to do a simple thing.

How did we even get here?


I have no goal to humiliate people from the ISO C ++ committee who chose this approach. I drank beer with some of them, and I can say that they are good, sensible and hardworking people. I’m sure that I’m missing some important context, for I have never been to the discussions of the standard and have not read all the accompanying documentation . But from the point of view of the external observer, the discrepancy between the complexity of the problem and the proposed solution is some kind of game. How can you teach this without overloading a beginner with a bunch of ... additional material? Should the average programmer know all this? And if the purpose of addingvariantIn the standard was not the creation of a tool for the mass user, is it really necessary? At a minimum, if the C ++ 17 committee did not have the time and resources to introduce pattern matching into the language, they should at least add something like that to the standard make_visitor. But this was left as an exercise for the user.

And yet, how have we come to such a life? I guess the thing is a person’s psychological inclination to confirm his point of view. Perhaps when there are some fairly competent people who know how SFINAE works , and do not get scared at the sight of something like this:

template <typename F>
typenamestd::enable_if<!std::is_reference<F>::value, int>::type
foo(F f){
    // ...
}

get together, they succeed std::visit. It would be madness to expect that a regular user of a language would fence an overloaded callable object with recursive templates in order to just understand intor stringbe stored in this thing.

I will not say that C ++ over-complexity is a blessing, but it is clearly much more complicated than it should be. Scott Meyers, author of the books Effective C ++ and Effective Modern C ++ , also expressed similar thoughts in recent lectures . Following Meyers, I am sure that every member of the committee is trying to avoid unnecessary complexity and to make the language as easy to use as possible. But it is difficult to say, looking at the result of their work. Uncritical complexity continues to grow.

Where are we going?


There is a reason why C ++ is used so widely, especially in system programming ( * ). It is extremely expressive, while leaving full control over the equipment. The toolkit created for C ++ is the most developed among all programming languages ​​except C. It supports an unimaginable number of platforms.

But if we put aside all this historical baggage, we will see some serious flaws. Take a look at D, and you will quickly realize that metaprogramming does not require self-torture and insane syntax. Play around a bit with Rust, and you feel that you unique_ptrandshared_ptr(which themselves were a breath of fresh air) look like a bad joke. It is quite obscene in 2017 to work with dependencies, literally copying the contents of one file to another using a directive #include.

Looking at what turns out to be in the standard and what we hear at conferences, it seems that the committee is trying to overcome these shortcomings by gradually dragging good solutions from other languages. At first glance, a good idea, but new features often come to the language as under-baked. Although C ++ is not going to retire in the near future, it seems that it always clumsily remains in the role of a catch-up.

Despite all this, I will convince my colleagues to use variantif there is a need for it. Sigma types are a very useful thing, and they are well worth the effort. howsaid John Kalb , "if you can't use the ugly wart language, you probably shouldn't write in C ++."

Notes


1. The term "$ \ Sigma $-type "came from type theory, with which you can describe typing in a programming language. If a type can contain either values ​​of type A or values ​​of type B, then the set of its possible states is the set-theoretic sum of all possible states of types A and B. You surely the “twins” of sigma-types are familiar: types-products , that is, structures, tuples, etc. For example, the set of possible states of a structure from types A and B contains the Cartesian product of states of types A and B.

2. There is a proposal P0095R1 on the introduction of pattern matching in C ++ Standard.

3. Obligatory to viewing report by Scott Meyers

Also popular now: