This question covers C++03, and how to omit a conversion operator from a class template, say template<typename T> struct Foo { ... };
, given traits on T
.
(Questions at the bottom)
Background
Ideally, I'd would like to make use of an enable_if
construct and SFINAE to exclude a conversion operator based on traits of T
of class template (see Foo
above), but default template arguments may not be used in function templates in C++03, which (afaik) excludes that approach; as previously covered in the following thread:
Instead, I'm using an approach where the type of the return value of the conversion operator is conditional on traits on T
, particularly being a dummy (void
or some private externally inaccessible type) for certain types of T
. This approach seems to work fine, but I'm uncertain what possible pitfalls I might be digging for myself; specifically what is guaranteed by the (C++03) Standard in this context.
Consider the following example (which I've tried to keep as minimal as possible), using the approach described in the previous paragraph:
include/util.h
:
namespace util {
// dummy predicate: is T int?
template <typename T> struct is_int { static const bool value = false; };
template <> struct is_int<int> { static const bool value = true; };
template <typename T> const bool is_int<T>::value;
// [meta.trans.other]/conditional
template <bool B, class T, class F> struct conditional { typedef T type; };
template <class T, class F> struct conditional<false, T, F> { typedef F type; };
// class template with non-template operator() member
template <typename T> struct Foo {
explicit Foo(const T &value) : value_(value) {}
// [Question regarding this conversion operator here]
operator typename conditional<is_int<T>::value, int, void>::type() const {
return value_;
}
private:
T value_;
};
/* Alternatively */
template <typename T> class Bar {
struct Dummy {};
T value_;
public:
explicit Bar(const T &value) : value_(value) {}
operator typename conditional<is_int<T>::value, int, Dummy>::type() const {
return value_;
}
};
} // namespace util
main.cc
:
#include "include/util.h"
void baz(int) {}
int main()
{
const util::Foo<int> foo_int(42);
baz(foo_int); // OK
const util::Foo<char> foo_char('a');
const util::Bar<int> bar_int(42);
baz(bar_int); // OK
const util::Bar<char> bar_char('a');
/* OK, expected:
Error: cannot convert ‘const util::Foo<char>’/‘const util::Bar<char>’
to ‘int’ for argument ‘1’ to ‘void baz(int)
baz(foo_char);
baz(bar_char); */
return 0;
}
This compiles fine using gcc
and clang
(-std=c++03
), but I'm wondering if it's really OK to conditionally have an invalid body(/return) for the conversion operator, as is the case e.g. for a full instantiation of Foo<char>
. I'm assuming it's fine due to partial implicit instantiation; [temp.inst]/1 (14.7.1 in the C++03 standard draft) describes [emphasis mine]:
The implicit instantiation of a class template specialization causes the implicit instantiation of the declarations, but not of the definitions or default arguments, of the class member functions, member classes, static data members and member templates; and it causes the implicit instantiation of the definitions of member anonymous unions. Unless a member of a class template or a member template has been explicitly instantiated or explicitly specialized, the specialization of the member is implicitly instantiated when the specialization is referenced in a context that requires the member definition to exist;
Question
T
) invalid non-template member function body of a class template is not an error in case it is not referenced from instantiations of the class where it is/would be invalid?Your reasoning is correct.
According to the paragraph cited ([temp.inst]/1), when Foo<T>
is implicitly instantiated for some Foo
, this does not instantiate the definitions of the member functions, and the member function definitions are not instantiated until the point when the definition is required to exist.
The definition is required to exist under the One Definition Rule. Specifically, under [basic.def.odr]/3:
Every program shall contain exactly one definition of every non-inline function or object that is used in that program; no diagnostic required. The definition can appear explicitly in the program, it can be found in the standard or a user-defined library, or (when appropriate) it is implicitly defined (see 12.1, 12.4 and 12.8). An inline function shall be defined in every translation unit in which it is used.
[basic.def.odr]/2 explains which functions are "used":
[...] An object or non-overloaded function is used if its name appears in a potentially-evaluated expression. A virtual member function is used if it is not pure. An overloaded function is used if it is selected by overload resolution when referred to from a potentially-evaluated expression. [Note: this covers calls to named functions (5.2.2), operator overloading (clause 13), user-defined conversions (12.3.2), allocation function for placement new (5.3.4), as well as non-default initialization (8.5). A copy constructor is used even if the call is actually elided by the implementation. ] [...]
(NB: In C++11, this concept was renamed to "odr-use".)
When you merely create an object of type util::Foo<char>
or util::Bar<char>
, that is not a use of any conversion function that such a class might have, so a definition is not required under the One Definition Rule. Finally, [temp.inst]/9 promises:
An implementation shall not implicitly instantiate a function template, a member template, a non-virtual member function, a member class or a static data member of a class template that does not require instantiation. [...]
So you are safe: the ill-formed body isn't instantiated.
Another way to conditionally declare a conversion function is to inherit from a CRTP base class that may or may not have the conversion function. This looks something like this (I haven't compiled it, so it may contain errors):
template <typename T, bool b>
struct Bar {};
template <typename T>
struct Bar<T, true> {
operator int() const {
return static_cast<const T*>(this)->value_;
}
};
template <typename T> struct Foo : Bar<Foo, is_int<T>::value> {
explicit Foo(const T &value) : value_(value) {}
friend class Bar<Foo, is_int<T>::value>;
private:
T value_;
};