Chapter 17

Future Directions

C++ templates have been evolving almost continuously from their initial design in 1988, through the various standardization milestones in 1998, 2011, 2014, and 2017. It could be argued that templates were at least somewhat related to most major language additions after the original 1998 standard.

The first edition of this book listed a number of extensions that we might see after the first standard, and several of those became reality:

• The angle bracket hack: C++11 removed the need to insert a space between two closing angle brackets.

• Default function template arguments: C++11 allows function templates to have default template arguments.

• Typedef templates: C++11 introduced alias templates, which are similar.

• The typeof operator: C++11 introduced the decltype operator, which fills the same role (but uses a different token to avoid a conflict with an existing extension that doesn’t quite meet the needs of the C++ programmers’ community).

• Static properties: The first edition anticipated a selection of type traits being supported directly by compilers. This has come to pass, although the interface is expressed using the standard library (which is then implemented using compiler extensions for many of the traits).

• Custom instantiation diagnostics: The new keyword static_assert implements the idea described by std::instantiation_error in the first edition of this book.

• List parameters: This became parameter packs in C++11.

• Layout control: C++11’s alignof and alignas cover the needs described in the first edition. Furthermore, the C++17 library added a std::variant template to support discriminated unions.

• Initializer deduction: C++17 added class template argument deduction, which addresses the same issue.

• Function expressions: C++11’s lambda expressions provides exactly this functionality (with a syntax somewhat different from that discussed in the first edition).

Other directions hypothesized in the first edition have not made it into the modern language, but most are still discussed on occasion and we keep their presentation in this volume. Meanwhile, other ideas are emerging and we present some of those as well.

17.1 Relaxed typename Rules

In the first edition of this book, this section suggested that the future might bring two kinds of relaxations to the rules for the use of typename (see Section 13.3.2 on page 228): Allow typename where it was not previously allowed, and make typename optional where a compiler can relatively easily infer that a qualified name with a dependent qualifier must name a type. The former came to pass (typename in C++11 can be used redundantly in many places), but the latter has not.

Recently, however, there has been a renewed call to make typename optional in various common contexts where the expectation of a type specifier is unambiguous:

• The return type and parameter types of function and member function declarations in namespace and class scope. Similarly with function and member function templates and with lambda expressions appearing in any scope.

• The types of variable, variable template, and static data member declarations. Again, similarly with variable templates.

• The type after the = token in an alias or alias template declaration.

• The default argument of a type parameter of a template.

• The type appearing in the angle brackets following a static_cast, const_cast, dynamic_cast, or reinterpret_cast, construct.

• The type named in a new expression.

Although this is a relatively ad hoc list, it turns out that such a change in the language would allow by far most instances of this use of typename to be dropped, which would make code more compact and more readable.

17.2 Generalized Nontype Template Parameters

Among the restrictions on nontype template arguments, perhaps the most surprising to beginning and advanced template writers alike is the inability to provide a string literal as a template argument.

The following example seems intuitive enough:

template<char const* msg>
class Diagnoser {
  public:
    void print();
};

int main()
{
    Diagnoser<"Surprise!">().print();
}

However, there are some potential problems. In standard C++, two instances of Diagnoser are the same type if and only if they have the same arguments. In this case the argument is a pointer value—in other words, an address. However, two identical string literals appearing in different source locations are not required to have the same address. We could thus find ourselves in the awkward situation that Diagnoser<"X"> and Diagnoser<"X"> are in fact two different and incompatible types! (Note that the type of "X" is char const[2], but it decays to char const* when passed as a template argument.)

Because of these (and related) considerations, the C++ standard prohibits string literals as arguments to templates. However, some implementations do offer the facility as an extension. They enable this by using the actual string literal contents in the internal representation of the template instance. Although this is clearly feasible, some C++ language commentators feel that a nontype template parameter that can be substituted by a string literal value should be declared differently from one that can be substituted by an address. One possibility would be to capture string literals in a parameter pack of characters. For example:

template<char… msg>
class Diagnoser {
  public:
    void print();
};

int main()
{
    // instantiates Diagnoser<’S’,’u’,’r’,’p’,’r’,’i’,’s’,’e’,’!’>
    Diagnoser<"Surprise!">().print();
}

We should also note an additional technical wrinkle in this issue. Consider the following template declarations, and let’s assume that the language has been extended to accept string literals as template arguments in this case:

template<char const* str>
class Bracket {
  public:
    static char const* address();
    static char const* bytes();
};

template<char const* str>
char const* Bracket<str>::address()
{
    return str;
}

template<char const* str>
char const* Bracket<str>::bytes()
{
    return str;
}

In the previous code, the two member functions are identical except for their names—a situation that is not that uncommon. Imagine that an implementation would instantiate Bracket<"X"> using a process much like macro expansion: In this case, if the two member functions are instantiated in different translation units, they may return different values. Interestingly, a test of some C++ compilers that currently provide this extension reveals that they do suffer from this surprising behavior.

A related issue is the ability to provide floating-point literals (and simple constant floating-point expressions) as template arguments. For example:

template<double Ratio>
class Converter {
  public:
    static double convert (double val) {
      return val*Ratio;
    }
};
using InchToMeter = Converter<0.0254>;

This too is provided by some C++ implementations and presents no serious technical challenges (unlike the string literal arguments).

C++11 introduced a notion of a literal class type: a class type that can take constant values computed at compile time (including nontrivial computations through constexpr functions). Once such class types became available, it quickly became desirable to allow them for nontype template parameters. However, problems similar to those of the string literal parameters described above arose. In particular, the “equality” of two class-type values is not a trivial matter, because it is in general determined by operator== definitions. This equality determines if two instantiations are equivalent, but in practice, that equivalence must be checkable by the linker by comparing mangled names. One way out may be an option to mark certain literal classes as having a trivial equality criterion that amounts to pairwise comparing of the scalar members of the class. Only class types with such a trivial equality criterion would then be permitted as nontype template parameter types.

17.3 Partial Specialization of Function Templates

In Chapter 16 we discussed how class templates can be partially specialized, whereas function templates are simply overloaded. The two mechanisms are somewhat different.

Partial specialization doesn’t introduce a completely new template: It is an extension of an existing template (the primary template). When a class template is looked up, only primary templates are considered at first. If, after the selection of a primary template, it turns out that there is a partial specialization of that template with a template argument pattern that matches that of the instantiation, its definition (in other words, its body) is instantiated instead of the definition of the primary template. (Full template specializations work exactly the same way.)

In contrast, overloaded function templates are separate templates that are completely independent of one another. When selecting which template to instantiate, all the overloaded templates are considered together, and overload resolution attempts to choose one as the best fit. At first this might seem like an adequate alternative, but in practice there are a number of limitations:

• It is possible to specialize member templates of a class without changing the definition of that class. However, adding an overloaded member does require a change in the definition of a class. In many cases this is not an option because we may not own the rights to do so. Furthermore, the C++ standard does not currently allow us to add new templates to the std namespace, but it does allow us to specialize templates from that namespace.

• To overload function templates, their function parameters must differ in some material way. Consider a function template R convert(T const&) where R and T are template parameters. We may very well want to specialize this template for R = void, but this cannot be done using overloading.

• Code that is valid for a nonoverloaded function may no longer be valid when the function is overloaded. Specifically, given two function templates f(T) and g(T) (where T is a template parameter), the expression g(&f<int>) is valid only if f is not overloaded (otherwise, there is no way to decide which f is meant).

• Friend declarations refer to a specific function template or an instantiation of a specific function template. An overloaded version of a function template would not automatically have the privileges granted to the original template.

Together, this list forms a compelling argument in support of a partial specialization construct for function templates.

A natural syntax for partially specializing function templates is the generalization of the class template notation:

template<typename T>
T const& max (T const&, T const&);               // primary template

template<typename T>
T* const& max <T*>(T* const&, T* const&);        // partial specialization

Some language designers worry about the interaction of this partial specialization approach with function template overloading. For example:

template<typename T>
void add (T& x, int i);     // a primary template

template<typename T1, typename T2>
void add (T1 a, T2 b);      // another (overloaded) primary template

template<typename T>
void add<T*> (T*&, int);    // Which primary template does this specialize?

However, we expect such cases would be deemed errors without major impact on the utility of the feature.

This extension was briefly discussed during the standardization of C++11 but gathered relatively little interest in the end. Still, the topic occasionally arises because it neatly solves some common programming problems. Perhaps it will be taken up again in some future version of the C++ standard.

17.4 Named Template Arguments

Section 21.4 on page 512 describes a technique that allows us to provide a nondefault template argument for a specific parameter without having to specify other template arguments for which a default value is available. Although it is an interesting technique, it is also clear that it results in a fair amount of work for a relatively simple effect. Hence, providing a language mechanism to name template arguments is a natural thought.

We should note at this point that a similar extension (sometimes called keyword arguments) was proposed earlier in the C++ standardization process by Roland Hartinger (see Section 6.5.1 of [StroustrupDnE]). Although technically sound, the proposal was ultimately not accepted into the language for various reasons. At this point there is no reason to believe named template arguments will ever make it into the language, but the topic arises regularly in committee discussions.

However, for the sake of completeness, we mention one syntactic idea that has been discussed:

template<typename T,
         typename Move = defaultMove<T>,
         typename Copy = defaultCopy<T>,
         typename Swap = defaultSwap<T>,
         typename Init = defaultInit<T>,
         typename Kill = defaultKill<T>>
class Mutator {
   …
};

void test(MatrixList ml)
{
   mySort (ml, Mutator <Matrix, .Swap = matrixSwap>);
}

Here, the period preceding the argument name is used to indicate that we’re referring to a template argument by name. This syntax is similar to the “designated initializer” syntax introduced in the 1999 C standard:

struct Rectangle {  int top, left, width, height; };
struct Rectangle r = { .width = 10, .height = 10, .top = 0, .left = 0 };

Of course, introducing named template arguments means that the template parameter names of a template are now part of the public interface to that template and cannot be freely changed. This could be addressed by a more explicit, opt-in syntax, such as the following:

template<typename T,
         Move: typename M = defaultMove<T>,
         Copy: typename C = defaultCopy<T>,
         Swap: typename S = defaultSwap<T>,
         Init: typename I = defaultInit<T>,
         Kill: typename K = defaultKill<T>>
class Mutator {
   …
};

void test(MatrixList ml)
{
   mySort (ml, Mutator <Matrix, .Swap = matrixSwap>);
}

17.5 Overloaded Class Templates

It is entirely possible to imagine that class templates could be overloaded on their template parameters. For example, one can imagine creating a family of Array templates that contains both dynamically and statically sized arrays:

template<typename T>
class Array {
    // dynamically sized array
    …
};

template<typename T, unsigned Size>
class Array {
    // fixed size array
    …
};

The overloading isn’t necessarily restricted to the number of template parameters; the kind of parameters can be varied too:

template<typename T1, typename T2>
class Pair {
   // pair of fields
    …
};

template<int I1, int I2>
class Pair {
    // pair of constant integer values
    …
};

Although this idea has been discussed informally by some language designers, it has not yet been formally presented to the C++ standardization committee.

17.6 Deduction for Nonfinal Pack Expansions

Template argument deduction for pack expansions only works when the pack expansion occurs at the end of the parameter or argument list. This means that while it is fairly simple to extract the first element from a list:

template<typename… Types>
struct Front;

template<typename FrontT, typename… Types>
struct Front<FrontT, Types…> {
  using Type = FrontT;
};

one cannot easily extract the last element of the list due to the restrictions placed on partial specializations described in Section 16.4 on page 347:

template<typename… Types>
struct Back;

template<typename BackT, typename… Types>
struct Back<Types…, BackT> { //ERROR: pack expansion not at the end of
  using Type = BackT;          //       template argument list
};

Template argument deduction for variadic function templates is similarly restricted. It is plausible that the rules regarding template argument deduction of pack expansions and partial specializations will be relaxed to allow the pack expansion to occur anywhere in the template argument list, making this kind of operation far simpler. Moreover, it is possible—albeit less likely—that deduction could permit multiple pack expansions within the same parameter list:

template<typename… Types> class Tuple {
};

template<typename T, typename… Types>
struct Split;

template<typename T, typename… Before, typename… After>
struct Split<T, Before…, T, After…> {
  using before = Tuple<Before…>;
  using after = Tuple<After…>;
};

Allowing multiple pack expansions introduces additional complexity. For example, does Split separate at the first occurrence of T, the last occurrence, or one in between? How complicated can the deduction process become before the compiler is permitted to give up?

17.7 Regularization of void

When programming templates, regularity is a virtue: If a single construct can cover all cases, it makes our template simpler. One aspect of our programs that is somewhat irregular are types. For example, consider the following:

auto&& r = f();        // error if f() returns void

That works for just about any type that f() returns except void. The same problem occurs when using decltype(auto):

decltype(auto) r = f();        // error if f() returns void

void is not the only irregular type: Function types and reference types often also exhibit behaviors that make them exceptional in some way. However, it turns out that void often complicates our templates and that there is no deep reason for void to be unusual that way. For example, see Section 11.1.3 on page 162 for an example how this complicates the implementation of a perfect std::invoke() wrapper.

We could just decree that void is a normal value type with a unique value (like std::nullptr_t for nullptr). For backward compatibility purposes, we’d still have to keep a special case for function declarations like the following:

void g(void);        // same as void g();

However, in most other ways, void would become a complete value type. We could then for example declare void variables and references:

void v = void{};
void&& rrv = f();

Most importantly, many templates would no longer need to be specialized for the void case.

17.8 Type Checking for Templates

Much of the complexity of programming with templates comes from the compiler’s inability to locally check whether a template definition is correct. Instead, most of the checking of a template occurs during template instantiation, when the template definition context and the template instantiation context are intertwined. This mixing of different contexts makes it hard to assign blame: Was the template definition at fault, because it used its template arguments incorrectly, or was the template user at fault, because the supplied template arguments didn’t meet the requirements of the template? The problem can be illustrated with a simple example, which we have annotated with the diagnostics produced by a typical compiler:

template<typename T>
T max(T a, T b)
{
  return b < a ? a : b;        
                          // ERROR: “no match for operator <
                          //        (operator types are ’X’ and ’X’)”
}

struct X {
};
bool operator> (X, X);

int main()
{
  X a, b;
  X m = max(a, b);        // NOTE: “in instantiation of function template specialization
                          //       ’max<X>’ requested here”
}

Note that the actual error (the lack of a proper operator<) is detected within the definition of the function template max(). It is possible that this is truly the error—perhaps max() should have used operator> instead? However, the compiler also provides a note that points to the place that caused the instantiation of max<X>, which may be the real error—perhaps max() is documented to require operator<? The inability to answer this question is what often leads to the “error novel” described in Section 9.4 on page 143, where the compiler provides the entire template instantiation history from the initial cause of the instantiation down to the actual template definition in which the error was detected. The programmer is then expected to determine which of the template definitions (or perhaps the original use of the template) is actually in error.

The idea behind type checking of templates is to describe the requirements of a template within the template itself, so that the compiler can determine whether the template definition or the template use is at fault when compilation fails. One solution to this problem is to describe the template’s requirements as part of the signature of the template itself using a concept:

template<typename T> requires LessThanComparable<T>
T
max(T a, T b)
{
   return b < a ? a : b;
}

struct X { };
bool operator> (X, X);

int main()
{
  X a, b;
  X m = max(a, b);        // ERROR: X does not meet the LessThanComparable requirement
}

By describing the requirements on the template parameter T, the compiler is able to ensure that the function template max() only uses operations on T that the user is expected to provide (in this case, LessThanComparable describes the need for operator<). Moreover, when using a template, the compiler can check that the supplied template argument provides all of the behavior required for the max() function template to work properly. By separating the type-checking problem, it becomes far easier for the compiler to provide an accurate diagnosis of the problem.

In the example above, LessThanComparable is called a concept: It represents constraints on a type (in the more general case, constraints on a set of types) that a compiler can check. Concept systems can be designed in different ways.

During the standardization cycle for C++11, an elaborate system was fully designed and implemented for concepts that are powerful enough to check both the point of instantiation of a template and the definition of a template. The former means, in our example above, that an error in main() could be caught early with a diagnostic explaining that X doesn’t satisfy the constraints of LessThanComparable. The latter means that when processing the max() template, the compiler checks that no operation not permitted by the LessThanComparable concept is used (and a diagnostic is emitted if that constraint is violated). The C++11 proposal was eventually pulled from the language specification because of various practical considerations (e.g., there were still many minor specification issues whose resolution was threatening a standard that was already running late).

After C++11 eventually shipped, a new proposal (first called concepts lite) was presented and developed by members of the committee. This system does not aim at checking the correctness of a template based on the constraints attached to it. Instead it focuses on the point of instantiations only. So if in our example max() were implemented using the > operator, no error would be issued at that point. However, an error would still be issued in main() because X doesn’t satisfy the constraints of LessThanComparable. The new concepts proposal was implemented and specified in what is called the Concepts TS (TS stands for Technical Specification), called C++ extensions for Concepts.1 Currently, the essential elements of that technical specification have been integrated into the draft for the next standard (expected to become C++20). Appendix E covers the language feature as specified in that draft at the time this book went to press.

17.9 Reflective Metaprogramming

In the context of programming, reflection refers to the ability to programmatically inspect features of the program (e.g., answering questions such as Is a type an integer? or What nonstatic data members does a class type contain?). Metaprogramming is the craft of “programming the program,” which usually amounts to programmatically generating new code. Reflective metaprogramming, then, is the craft of automatically synthesizing code that adapts itself to existing properties (often, types) of a program.

In Part III of this book, we will explore how templates can achieve some simple forms of reflection and metaprogramming (in some sense, template instantiation is a form of metaprogramming, because it causes the synthesis of new code). However, the capabilities of C++17 templates are rather limited when it comes to reflection (e.g., it is not possible to answer the question What nonstatic data members does a class type contain?) and the options for metaprogramming are often inconvenient in various ways (in particular, the syntax becomes unwieldy and the performance is disappointing).

Recognizing the potential of new facilities in this area, the C++ standardization committee created a study group (SG7) to explore options for more powerful reflection. That group’s charter was later extended to cover metaprogramming also. Here is an example of one of the options being considered:

template<typename T> void report(T p) {
  constexpr {
    std::meta::info infoT = reflexpr(T);
    for (std::meta::info : std::meta::data_members(infoT)) {
      -> {
          std::cout << (: std::meta::name(info) :)
                    << ":  " << p.(.info.) << ’ ’;
      }
    }
  }

  //code will be injected here
}

Quite a few new things are present in this code. First, the constexpr{} construct forces the statements in it to be evaluated at compile time, but if it appears in a template, this evaluation is only done when the template is instantiated. Second, the reflexpr() operator produces an expression of opaque type std::meta::info that is a handle to reflected information about its argument (the type substituted for T in this example). A library of standard metafunctions permits querying this meta-information. One of those standard metafunctions is std::meta::data_members, which produces a sequence of std::meta::info objects describing the direct nonstatic data members of its operand. So the for loop above is really a loop over the nonstatic data members of p.

At the core of the metaprogramming capabilities of this system is the ability to “inject” code in various scopes. The construct ->{} injects statements and/or declarations right after the statement or declaration that kicked off a constexpr evaluation. In this example, that means after the constexpr{} construct. The code fragments being injected can contain certain patterns to be replaced by computed values. In this example, (::) produces a string literal value (the expression std::meta::name(info) produces a string-like object representing the unqualified name of the entity—data member in this case—represented by info). Similarly, the expression (.info.) produces an identifier naming the entity represented by info. Other patterns to produce types, template argument lists, etc. are also proposed.

With all that in place, instantiating the function template report() for a type:

struct X {
  int x;
  std::string s;
};

would produce an instantiation similar to

template<> void report(X const& p) {
    std::cout << "x" << ": " << "p.x" << ’ ’;
    std::cout << "s" << ": " << "p.s" << ’ ’;
}

That is, this function automatically generates a function to output the nonstatic data member values of a class type.

There are many applications for these types of capabilities. While it is likely that something like this will eventually be adopted into the language, it is unclear in what time frame it can be expected. That said, a few experimental implementations of such systems have been demonstrated at the time of this writing. (Just before going to press with this book, SG7 agreed on the general direction of using constexpr evaluation and a value type somewhat like std::meta::info to deal with reflective metaprogramming. The injection mechanism presented here, however, was not agreed on, and most likely a different system will be pursued.)

17.10 Pack Facilities

Parameter packs were introduced in C++11, but dealing with them often requires recursive template instantiation techniques. Recall this outline of code discussed in Section 14.6 on page 263:

template<typename Head, typename… Remainder>
void f(Head&& h, Remainder&&… r) {
  doSomething(h);
  if constexpr (sizeof…(r) != 0) {
    // handle the remainder recursively (perfectly forwarding the arguments):
    f(r…);
  }
}

This example is made simpler by exploiting the features of the C++17 compile-time if statement (see Section 8.5 on page 134), but it remains a recursive instantiation technique that may be somewhat expensive to compile.

Several committee proposals have tried to simplify this state of affairs somewhat. One example is the introduction of a notation to pick a specific element from a pack. In particular, for a pack P the notation P.[N] has been suggested as a way to denote element N+1 of that pack. Similarly, there have been proposals to denote “slices” of packs (e.g., using the notation P.[b, e]).

While examining such proposals, it has become clear that they interact somewhat with the notion of reflective metaprogramming discussed above. It is unclear at this time whether specific pack selection mechanisms will be added to the language or whether metaprogramming facilities covering this need will be provided instead.

17.11 Modules

Another upcoming major extension, modules, is only peripherally related to templates, but it is still worth mentioning here because template libraries are among the greatest beneficiaries of them.

Currently, library interfaces are specified in header files that are textually #included into translation units. There are several downsides to this approach, but the two most objectionable ones are that (a) the meaning of the interface text may be accidentally modified by previously included code (e.g., through macros), and (b) reprocessing that text every time quickly dominates build times.

Modules are a feature that allows library interfaces to be compiled into a compiler-specific format, and then those interfaces can be “imported” into translation units without being subject to macro expansion or modification of the meaning of code by the accidental presence of addition declarations. What’s more, a compiler can arrange to read only those parts of a compiled module file that are relevant to the client code, thereby drastically accelerating the compilation process.

Here is what a module definition may look like:

module MyLib;

void helper() {
  …
}
export inline void libFunc() {

  …
  helper();
  …
}

This module exports a function libFunc() that can be used in client code as follows:

import MyLib;
int main() {
  libFunc();
}

Note that libFunc() is made visible to client code but the function helper() is not, even though the compiled module file will likely contain information about helper() to enable inlining.

The proposal to add modules to C++ is well on its way, and the standardization committee is aiming at integrating it after C++17. One of the concerns in developing such a proposal is how to transition from a world of header files to a world of modules. There are already facilities to enable this to some degree (e.g., the ability to include header files without making their contents part of the module), and additional ones still under discussion (e.g., the ability to export macros from modules).

Modules are particularly useful for template libraries because templates are almost always fully defined in header files. Even including a basic standard header like <vector> amounts to processing tens of thousands of lines of C++ code (even when only a small number of the declarations in that header will be referenced). Other popular libraries increase this by an order of magnitude. Avoiding the costs of all this compilation will be of major interest to the C++ programmers dealing with large, complex code bases.

1 See, for example, document N4641 for the version of the Concepts TS in the beginning of 2017.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.21.34.0