C++14 Variable Templates: what is their purpose? A

2019-01-13 07:39发布

问题:

C++14 will allow the creation of variables that are templated. The usual example is a variable 'pi' that can be read to get the value of the mathematical constant π for various types (3 for int; the closest value possible with float, etc.)

Besides that we can have this feature just by wrapping a variable within a templated struct or class, how does this mix with type conversions? I see some overlapping.

And other than the pi example, how would it work with non-const variables? Any usage example to understand how to make the most of such a feature and what its purpose is?

回答1:

And other than the pi example, how would it work with non-const variables?

Currently, it seems to instantiate the variables separately for the type. i.e., you could assign 10 to n<int> and it would be different from the template definition.

template<typename T>
T n = T(5);

int main()
{
    n<int> = 10;
    std::cout << n<int> << " ";    // 10
    std::cout << n<double> << " "; // 5
}

If the declaration is const, it is readonly. If it's a constexpr, like all constexpr declarations, it has not much use outside constexpr(ressions).

Besides that we can have this feature just by wrapping a variable within a templated struct or class, how does this mix with type conversions?

It's meant to be a simple proposal. I am unable to see how it affects type conversions in a significant way. As I already stated, the type of the variable is the type you instantiated the template with. i.e., decltype(n<int>) is int. decltype((double)n<int>) is double and so on.

Any usage example to understand how to make the most of such a feature and what its purpose is?

N3651 provides a succinct rationale.

Alas, existing C++ rules do not allow a template declaration to declare a variable. There are well known workarounds for this problem:

• use constexpr static data members of class templates

• use constexpr function templates returning the desired values

These workarounds have been known for decades and well documented. Standard classes such as std::numeric_limits are archetypical examples. Although these workarounds aren’t perfect, their drawbacks were tolerable to some degree because in the C++03 era only simple, builtin types constants enjoyed unfettered direct and efficient compile time support. All of that changed with the adoption of constexpr variables in C++11, which extended the direct and efficient support to constants of user-defined types. Now, programmers are making constants (of class types) more and more apparent in programs. So grow the confusion and frustrations associated with the workarounds.

...

The main problems with "static data member" are:

• they require "duplicate" declarations: once inside the class template, once outside the class template to provide the "real" definition in case the con- stants is odr-used.

• programmers are both miffed and confused by the necessity of providing twice the same declaration. By contrast, "ordinary" constant declarations do not need duplicate declarations.

...

Well known examples in this category are probably static member functions of numeric_limits, or functions such as boost::constants::pi<T>(), etc. Constexpr functions templates do not suffer the "duplicate declarations" issue that static data members have; furthermore, they provide functional abstraction. However, they force the programmer to chose in advance, at the definition site, how the constants are to be delivered: either by a const reference, or by plain non- reference type. If delivered by const reference then the constants must be systematically be allocated in static storage; if by non-reference type, then the constants need copying. Copying isn’t an issue for builtin types, but it is a showstopper for user-defined types with value semantics that aren’t just wrappers around tiny builtin types (e.g. matrix, or integer, or bigfloat, etc.) By contrast, "ordinary" const(expr) variables do not suffer from this problem. A simple definition is provided, and the decision of whether the constants actually needs to be layout out in storage only depends on the usage, not the definition.



回答2:

we can have this feature just by wrapping a variable within a templated struct or class

Yes, but that would be gratuitous syntactic salt. Not healthy for the blood pressure.

pi<double> conveys the intent better than pi<double>::value. Short and to the point. That's enough of a reason in my book to allow and encourage this syntax.



回答3:

I wonder whether something along these lines would be possible: (assuming availability of template lambdas)

void some_func() {
    template<typename T>
    std::map<int, T> storage;

    auto store = []<typename T>(int key, const T& value) { storage<T>[key] = value; };

    store(0, 2);
    store(1, "Hello"s);
    store(2, 0.7);

    // All three values are stored in a different map, according to their type. 
}

Now, is this useful?

As a simpler use, notice that the initialization of pi<T> uses explicit conversion (explicit call of a unary constructor) and not uniform initialization. Which means that, given a type radians with a constructor radians(double), you can write pi<radians>.



回答4:

Another practical example for C++14's variable templates is when you need a function for passing something into std::accumulate:

template<typename T>
T const & (*maxer) (T const &, T const &) = std::max<T>;

std::accumulate(some.begin(), some.end(), initial, maxer<float>);

Note that using std::max<T> is insufficient because it can't deduce the exact signature. In this particular example you can use max_element instead, but the point is that there is a whole class of functions that share this behavior.



回答5:

Well, you can use this to write compile time code like this:

#include <iostream>

template <int N> const int ctSquare = N*N;

int main() {
    std::cout << ctSquare<7> << std::endl;
}

This is a significant improvement over the equivalent

#include <iostream>

template <int N> struct ctSquare {
    static const int value = N*N;
};

int main() {
    std::cout << ctSquare<7>::value << std::endl;
}

that people used to write to perform template metaprogramming before variable templates were introduced. For non-type values, we were able to do this since C++11 with constexpr, so template variables have only the advantage of allowing computations based on types to the variable templates.

TL;DR: They don't allow us to do anything we couldn't do before, but they make template metaprogramming less of a PITA.