T.interfaces: Template interfaces
???
T.40: Use function objects to pass operations to algorithms
Reason
Function objects can carry more information through an interface than a "plain" pointer to function. In general, passing function objects gives better performance than passing pointers to functions.
Example
bool greater(double x, double y) { return x>y; }
sort(v, greater); // pointer to function: potentially slow
sort(v, [](double x, double y) { return x>y; }); // function object
sort(v, greater<>); // function object
bool greater_than_7(double x) { return x>7; }
auto x = find_if(v, greater_than_7); // pointer to function: inflexible
auto y = find_if(v, [](double x) { return x>7; }); // function object: carries the needed data
auto z = find_if(v, Greater_than<double>(7)); // function object: carries the needed data
You can, of course, gneralize those functions using auto
or (when and where available) concepts. For example:
auto y1 = find_if(v, [](Ordered x) { return x>7; }); // reruire an ordered type
auto z1 = find_if(v, [](auto x) { return x>7; }); // hope that the type has a >
Note
Lambdas generate function objects.
Note
The performance argument depends on compiler and optimizer technology.
Enforcement
- Flag pointer to function template arguments.
- Flag pointers to functions passed as arguments to a template (risk of false positives).
T.41: Require complete sets of operations for a concept
Reason
Ease of comprehension. Improved interoperability. Flexibility for template implementers.
Note
The issue here is whether to require the minimal set of operations for a template argument
(e.g., ==
but not !=
or +
but not +=
).
The rule supports the view that a concept should reflect a (mathematically) coherent set of operations.
Example, bad
class Minimal {
// ...
};
bool operator==(const Minimal&,const Minimal&);
bool operator<(const Minimal&,const Minimal&);
Minimal operator+(const Minimal&, const Minimal&);
// no other operators
void f(const Minimal& x, const Minimal& y)
{
if (!(x==y) { /* ... */ } // OK
if (x!=y) { /* ... */ } //surprise! error
while (!(x<y)) { /* ... */ } // OK
while (x>=y) { /* ... */ } //surprise! error
x = x+y; // OK
x += y; // surprise! error
}
This is minimal, but surprising and constraining for users. It could even be less efficient.
Example
class Convenient {
// ...
};
bool operator==(const Convenient&,const Convenient&);
bool operator<(const Convenient&,const Convenient&);
// ... and the other comparison operators ...
Minimal operator+(const Convenient&, const Convenient&);
// .. and the other arithmetic operators ...
void f(const Convenient& x, const Convenient& y)
{
if (!(x==y) { /* ... */ } // OK
if (x!=y) { /* ... */ } //OK
while (!(x<y)) { /* ... */ } // OK
while (x>=y) { /* ... */ } //OK
x = x+y; // OK
x += y; // OK
}
It can be a nuisance to define all operators, but not hard. Hopefully, C++17 will give you comparison operators by default.
Enforcement
- Flag classes the support "odd" subsets of a set of operators, e.g.,
==
but not!=
or+
but not-
. Yes,std::string
is "odd", but it's too late to change that.
T.42: Use template aliases to simplify notation and hide implementation details
Reason
Improved readability. Implementation hiding. Note that template aliases replace many uses of traits to compute a type. They can also be used to wrap a trait.
Example
template<typename T, size_t N>
class matrix {
// ...
using Iterator = typename std::vector<T>::iterator;
// ...
};
This saves the user of Matrix
from having to know that its elements are stored in a vector
and also saves the user from repeatedly typing typename std::vector<T>::
.
Example
template<typename T>
using Value_type = typename container_traits<T>::value_type;
This saves the user of Value_type
from having to know the technique used to implement value_type
s.
Enforcement
- Flag use of
typename
as a disambiguator outsideusing
declarations. - ???
T.43: Prefer using
over typedef
for defining aliases
Reason
Improved readability: With using
, the new name comes first rather than being embedded somewhere in a declaration.
Generality: using
can be used for template aliases, whereas typedef
s can't easily be templates.
Uniformity: using
is syntactically similar to auto
.
Example
typedef int (*PFI)(int); // OK, but convoluted
using PFI2 = int (*)(int); // OK, preferred
template<typename T>
typedef int (*PFT)(T); // error
template<typename T>
using PFT2 = int (*)(T); // OK
Enforcement
- Flag uses of
typedef
. This will give a lot of "hits" :-(
T.44: Use function templates to deduce class template argument types (where feasible)
Reason
Writing the template argument types explicitly can be tedious and unnecessarily verbose.
Example
tuple<int, string, double> t1 = {1, "Hamlet", 3.14}; // explicit type
auto t2 = make_tuple(1, "Ophelia"s, 3.14); // better; deduced type
Note the use of the s
suffix to ensure that the string is a std::string
, rather than a C-style string.
Note
Since you can trivially write a make_T
function, so could the compiler. Thus, make_T
functions may become redundant in the future.
Exception
Sometimes there isn't a good way of getting the template arguments deduced and sometimes, you want to specify the arguments explicitly:
vector<double> v = { 1, 2, 3, 7.9, 15.99 };
list<Record*> lst;
Enforcement
Flag uses where an explicitly specialized type exactly matches the types of the arguments used.
T.46: Require template arguments to be at least Regular
or SemiRegular
Reason
Readability. Preventing surprises and errors. Most uses support that anyway.
Example
class X {
// ...
public:
explicit X(int);
X(const X&); // copy
X operator=(const X&);
X(X&&); // move
X& operator=(X&&);
~X();
// ... no moreconstructors ...
};
X x {1}; // fine
X y = x; // fine
std::vector<X> v(10); // error: no default constructor
Note
Semiregular requires default constructible.
Enforcement
- Flag types that are not at least
SemiRegular
.
T.47: Avoid highly visible unconstrained templates with common names
Reason
An unconstrained template argument is a perfect match for anything so such a template can be preferred over more specific types that require minor conversions. This is particularly annoying/dangerous when ADL is used. Common names make this problem more likely.
Example
namespace Bad {
struct S { int m; };
template<typename T1, typename T2>
bool operator==(T1, T2) { cout << "Bad\n"; return true; }
}
namespace T0 {
bool operator==(int, Bad::S) { cout << "T0\n"; return true; } // compate to int
void test()
{
Bad::S bad{ 1 };
vector<int> v(10);
bool b = 1==bad;
bool b2 = v.size()==bad;
}
}
This prints T0
and Bad
.
Now the ==
in Bad
was designed to cause trouble, but would you have spotted the problem in real code?
The problem is that v.size()
returns an unsigned
integer so that a conversion is needed to call the local ==
;
the ==
in Bad
requires no conversions.
Realistic types, such as the standard library iterators can be made to exhibit similar anti-social tendencies.
Enforcement
????
T.48: If your compiler does not support concepts, fake them with enable_if
Reason
???
Example
???
Enforcement
???
T.49: Where possible, avoid type-erasure
Reason
Type erasure incurs an extra level of indirection by hiding type information behind a separate compilation boundary.
Example
???
Exceptions: Type erasure is sometimes appropriate, such as for std::function
.
Enforcement
???
T.50: Avoid writing an unconstrained template in the same namespace as a type
Reason
ADL will find the template even when you think it shouldn't.
Example
???
Note
This rule should not be necessary; the committee cannot agree on how to fix ADL, but at least making it not consider unconstrained templates would solve many of the actual problems and remove the need for this rule.
Enforcement
??? unfortunately this will get many false positives; the standard library violates this widely, by putting many unconstrained templates and types into the single namespace std