C++
Why are default template arguments only allowed on class templates
C++ templates are a cornerstone of generic programming, allowing developers to write flexible and reusable code that works with various data types. However, a common point of confusion for many C++ practitioners, especially those delving deeper into template metaprogramming, is the rule that default template arguments are only allowed on class templates, not function templates. This distinction isn’t arbitrary; it stems from fundamental differences in how class templates and function templates are instantiated and, critically, how type deduction operates. Understanding this design choice is key to mastering advanced C++ and appreciating the language’s elegant yet complex type system. Let’s explore the underlying reasons and the implications for your code.
The Core Distinction: Class vs. Function Templates
At their heart, both class templates and function templates serve the purpose of generic programming, enabling algorithms and data structures to operate independently of specific data types. Yet, their mechanisms for achieving this generality diverge significantly. Class templates are blueprints for generating classes, where the types are typically specified explicitly by the user or inferred from default arguments. For instance, a std::vector<int> explicitly states its element type.
Function templates, on the other hand, are blueprints for generating functions. Their primary strength lies in automatic type deduction, a powerful feature where the compiler infers the template arguments based on the types of the function call arguments. When you call std::sort(myVector.begin(), myVector.end()), the compiler automatically deduces the iterator and element types without you needing to specify them. This difference in how template arguments are provided or deduced is central to understanding the restriction on default template arguments.
Type Deduction in Function Templates
Function template type deduction is a sophisticated process where the compiler analyzes the arguments passed to a function template call and attempts to determine the template parameters. This process is often implicit, making function templates incredibly convenient to use. For example, if you have a function template template <typename T> void print(T val) and you call print(5), the compiler deduces T to be int. This automatic inference is efficient and reduces boilerplate code, but it also introduces complexities when considering default template arguments.
Explicit Instantiation in Class Templates
Class templates typically require explicit specification of their template arguments, such as std::map<std::string, int>. While C++17 introduced Class Template Argument Deduction (CTAD), allowing some class templates to deduce arguments from constructor calls (e.g., std::vector v = {1, 2, 3};), the fundamental mechanism for class templates still leans towards explicit or fully deduced arguments at the point of instantiation. This design allows for default template arguments to fill in missing types without ambiguity, as there isn’t a complex type deduction process running in parallel that could conflict.
The Role of Type Deduction in Function Templates
The primary reason default template arguments are only allowed on class templates is rooted in the intricate process of type deduction for function templates. When you call a function template, the compiler’s main task is to figure out the concrete types for all template parameters. If a function template were allowed to have default template arguments, it would create an ambiguity for the compiler during type deduction.
Consider a hypothetical scenario: template <typename T = int> void func(T arg);. If you call func(5.0), the compiler would deduce T as double from the argument. But what if you call func() with no arguments? The compiler would then have to decide whether to use the default T = int or attempt to deduce it from a non-existent argument. This creates a logical conflict: type deduction expects arguments to infer types, while default arguments provide types when arguments are absent or ambiguous. The C++ standard prioritizes the robustness and predictability of type deduction for function templates, which is a cornerstone of generic algorithms.
The compiler uses a set of rules for template argument deduction, including exact matches, promotions, and conversions. Adding default template arguments to this mix would significantly complicate the overload resolution process and potentially lead to surprising and hard-to-debug behavior. As explained by isocpp.org, the official home of the C++ Standard, the language strives for clear and unambiguous behavior, especially in core features like templates. Allowing defaults on function templates would undermine this clarity, potentially making it impossible for the compiler to consistently choose the “best” function.
- Argument Matching: The compiler first tries to match function call arguments to function template parameters.
- Type Inference: Based on these matches, it infers the types for the template parameters.
- Default Conflict: If default template arguments were present, the compiler would face a dilemma when an argument could be deduced OR provided by a default.
- Ambiguity Avoidance: To prevent such ambiguities and maintain predictable behavior, the C++ standard disallows default template arguments for function templates.
This design choice ensures that when you see a function template call, you can confidently trace how its types are determined, either explicitly provided or deduced from the arguments. This predictability is vital for complex generic libraries and template metaprogramming, where precise type control is paramount. For further insights into C++ template mechanics, you might find this resource on understanding C++ template specialization helpful.
How Default Arguments Simplify Class Template Usage
While function templates thrive on automatic type deduction, class templates benefit immensely from default template arguments, which significantly enhance usability and flexibility. These defaults allow developers to specify common or sensible default types for template parameters, reducing the verbosity required to instantiate a class template. This is particularly valuable for complex data structures or utility classes where certain types are frequently used.
For example, std::vector is defined as template <class T, class Allocator = std::allocator<T>> class vector;. Here, std::allocator<T> is a default argument for the Allocator template parameter. This means you can simply write std::vector<int> myVec; instead of the more verbose std::vector<int, std::allocator<int>> myVec;. The default argument provides a sensible standard without requiring the user to explicitly specify it unless a custom allocator is needed. This significantly cleans up code and makes standard library components much easier to adopt.
- Reduced Verbosity: Users don’t need to specify every template argument if a default is suitable.
- Improved Readability: Code becomes cleaner and easier to understand, focusing on the essential types.
- Sensible Defaults: Library designers can provide commonly used or recommended types out-of-the-box.
- Enhanced Flexibility: Users can still override default arguments when custom behavior is required.
Compiler’s Perspective and Language Design
From the compiler’s viewpoint, the distinction between class and function templates regarding default arguments is a matter of maintaining consistency and avoiding ambiguity in the language. The C++ standard committee, when designing these features, had to weigh the benefits of increased Question & Answer :
Why are default template arguments only allowed on class templates? Why can’t we define a default type in a member function template? For example:
struct my_class { template<class T = int> void mymember(T* vec) { // ... } };
Instead, C++ forces that default template arguments are only allowed on a class template.
It makes sense to give default template arguments. For example you could create a sort function:
template<typename Iterator, typename Comp = std::less< typename std::iterator_traits<Iterator>::value_type> > void sort(Iterator beg, Iterator end, Comp c = Comp()) { ... }
C++0x introduces them to C++. See this defect report by Bjarne Stroustrup: Default Template Arguments for Function Templates and what he says
The prohibition of default template arguments for function templates is a misbegotten remnant of the time where freestanding functions were treated as second class citizens and required all template arguments to be deduced from the function arguments rather than specified.
The restriction seriously cramps programming style by unnecessarily making freestanding functions different from member functions, thus making it harder to write STL-style code.