Will Specialization of Function Templates in Std for Program-Defined Types No Longer Be Allowed in C++20

Will specialization of function templates in std for program-defined types no longer be allowed in C++20?

As it stands now it definitly looks that way. Previously [namespace.std] contained

A program may add a template specialization for any standard library template to namespace std only if the declaration depends on a user-defined type and the specialization meets the standard library requirements for the original template and is not explicitly prohibited.

While the current draft states

Unless explicitly prohibited, a program may add a template specialization for any standard library class template to namespace std provided that (a) the added declaration depends on at least one program-defined type and (b) the specialization meets the standard library requirements for the original template.

emphasis mine

And it looks like the paper Thou Shalt Not Specialize std Function Templates! by Walter E. Brown is responsible for it. In it he details an number of reason why this should be changed such as:

  • Herb Sutter: “specializations don’t participate in overloading. [...] If you want to customize a function base template and want that
    customization to participate in overload resolution (or, to always be
    used in the case of exact match), make it a plain old function, not a
    specialization. And, if you do provide overloads, avoid also providing
    specializations.”
  • David Abrahams: “it’s wrong to use function template specialization [because] it interacts in bad ways with overloads. [...] For example,
    if you specialize the regular std::swap for std::vector<mytype>&,
    your specialization won’t get chosen over the standard’s vector
    specific swap, because specializations aren’t considered during
    overload resolution.”
  • Howard Hinnant: “this issue has been settled for a long time. . . . Disregard Dave’s expert opinion/answer in this area at your own
    peril.”
  • Eric Niebler: “[because of] the decidedly wonky way C++ resolves function calls in templates. . . , [w]e make an unqualified call to
    swap in order to find an overload that might be defined in [...]
    associated namespaces[...] , and we do using std::swap so that, on
    the off-chance that there is no such overload, we find the default
    version defined in the std namespace.”
  • High Integrity C++ Coding Standard: “Overload resolution does not take into account explicit specializations of function templates. Only
    after overload resolution has chosen a function template will any
    explicit specializations be considered.”

Why isn't explicit specialization with private type allowed for function templates?

From C++20, using private members in the parameter of a specialization of a function template is perfectly valid, due to PR0692. In particular, the following wording was added to temp.spec.general#6:

The usual access checking rules do not apply to names in a declaration of an explicit instantiation or explicit specialization, with the exception of names appearing in a function body, default argument, base-clause, member-specification, enumerator-list, or static data member or variable template initializer.

[ Note 1: In particular, the template arguments and names used in the function declarator (including parameter types, return types and exception specifications) can be private types or objects that would normally not be accessible. - end note]

(emphasis mine)

The code not compiling is due to a GCC bug 97942, and it compiles just fine in Clang. demo.

Is it legal in C++ to specialize std::minus for user-defined types?

The rules for extending namespace std says:

It is allowed to add template specializations for any standard library class template to the namespace std only if the declaration depends on at least one program-defined type and the specialization satisfies all requirements for the original template, except where such specializations are prohibited.

(emphasis mine)

So in your case, adding a specialization for the program-defined type MyArrayType would work.

However, the return type of the member operator() must be MyArrayType, not double. Also, the member operator() must be constexpr.

Violating either of these means the specialization doesn't satisfy the requirements of the primary template.

Allow Only Explicit Specialization of Template Class

There are a few ways how you could accomplish this.

1. Explicitly list the acceptable enums

One way would be to explicitly list the acceptable enums in your static_assert:

godbolt

#include <type_traits>

template<class T, class... Other>
constexpr bool is_same_one_of = (std::is_same_v<T, Other> || ...);

enum Enum1 {};
enum Enum2 {};
enum Enum3 {};

template<class T>
class kvEnumHelper {
static_assert(
is_same_one_of<T, Enum1, Enum2 /* , ... more enum types ... */>,
"T must be either Enum1 or Enum2"
);

/* ... actual implementation ... */
};

kvEnumHelper<Enum1> foo1; // ok
kvEnumHelper<Enum2> foo2; // ok
kvEnumHelper<Enum3> foo3; // compile error


2. Inherit implementation

Another option would be to move the actual implementation into a separate class and only make the specializations inherit from the implementation class.

godbolt

#include <type_traits> 

enum Enum1 {};
enum Enum2 {};
enum Enum3 {};

template<class T>
class kvEnumHelper {
static_assert(
!std::is_same_v<T, T>, // always false, but only when actually instanciated
"Enum Class is not supported"
);
};

template<class TEnum>
class kvEnumHelperImpl {
/* ... actual implementation ... */
};

template<> class kvEnumHelper<Enum1> : public kvEnumHelperImpl<Enum1> {};
template<> class kvEnumHelper<Enum2> : public kvEnumHelperImpl<Enum2> {};

kvEnumHelper<Enum1> foo1; // ok
kvEnumHelper<Enum2> foo2; // ok
kvEnumHelper<Enum3> foo3; // compile error


3. Using an additional trait

Yet another alternative would be to use a trait that can specialized for the enum types you would want to be usable with kvEnumHelper.

godbolt

template <class T>
constexpr bool allow_enum_helper = false;

enum Enum1 {};
enum Enum2 {};
enum Enum3 {};

template<class T>
class kvEnumHelper {
static_assert(
allow_enum_helper<T>,
"Enum Class is not supported"
);

/* ... actual implementation ... */
};

template<>
constexpr bool allow_enum_helper<Enum1> = true;
template<>
constexpr bool allow_enum_helper<Enum2> = true;

kvEnumHelper<Enum1> foo1; // ok
kvEnumHelper<Enum2> foo2; // ok
kvEnumHelper<Enum3> foo3; // compile error

If you already have a function like getEnumFromString that is deleted and has specializations for the allowable enum types you could use that to detect if kvEnumHelper<T> should be allowed by detecting if the function is deleted or not.

godbolt

#include <string>

enum Enum1 {};
enum Enum2 {};
enum Enum3 {};

template<typename T>
T getEnumFromString(const std::string& in_string) = delete; // only allow templates we define (catches them at compile time)
template<> Enum1 getEnumFromString(const std::string& in_string);
template<> Enum2 getEnumFromString(const std::string& in_string);

template<class T>
constexpr bool allow_enum_helper = requires { getEnumFromString<T>(std::string{}); };

template<class T>
class kvEnumHelper {
static_assert(
allow_enum_helper<T>,
"Enum Class is not supported"
);

/* ... actual implementation ... */
};

kvEnumHelper<Enum1> foo1; // ok
kvEnumHelper<Enum2> foo2; // ok
kvEnumHelper<Enum3> foo3; // compile error

Specializing types from namespace std based on user-defined concepts

The general rule we have, in [namespace.std]/2 is:

Unless explicitly prohibited, a program may add a template specialization for any standard library class template to namespace std provided that (a) the added declaration depends on at least one program-defined type and (b) the specialization meets the standard library requirements for the original template.

For this specialization:

template<MyConcept T>
struct hash<T> { /* ... */ };

If it is the case that (a) for all T that satisfy MyConcept, T depends on at least one program-defined type and (b) this specialization meets the requirements for hash (i.e. it's invocable, swappable, satisfies the normal hash requirements, etc.), then this is fine.

By depends on at least one program-defined type, it's not just that it's like my::Type, but it could also include std::vector<my::Type> and std::tuple<int, char, my::Type*>, etc. As long as something in there somewhere is program-defined.

Is it legal to specialize std library functions for a shared_ptr of a user defined type?

Yes, that is legal.

It is even questionably legal to specialize for std::shared_ptr<int> at one point; I don't know if they patched that ambiguity in the standard as a defect or not.

Note that that is a poor implemenation of a hash for global use. First, because it doesn't support null shared pointers. Second, because hashing a shared pointer as always the int value is questionable. It is even dangerous, because if a shared pointer to an int in a container has that int change, you just broke the program.

Consider making your own hasher for these kind of cases.

namespace notstd {
template<class T, class=void>
struct hasher_impl:std::hash<T>{};

namespace adl_helper {
template<class T>
std::size_t hash( T const& t, ... ) {
return ::notstd::hasher_impl<T>{}(t);
}
};
namespace adl_helper2 {
template<class T>
std::size_t hash_helper(T const& t) {
using ::notstd::adl_helper::hash;
return hash(t);
}
}
template<class T>
std::size_t hash(T const& t) {
return ::notstd::adl_helper2::hash_helper(t);
}

struct hasher {
template<class T>
std::size_t operator()(T const& t)const {
return hash(t);
}
};

}

Now this permits 3 points of customization.

First, if you override std::size_t hash(T const&) in the namespace containing T, it picks it up.

Failing that, if you specialize notstd::hasher_impl<T, void> for your type T, it picks it up.

Third, if both of those fail, it invokes std::hash<T>, picking up any specializations.

Then you can do:

std::unordered_set<std::shared_ptr<MyType>, ::notstd::hasher> mySet;

and add:

struct MyType {
MyType(std::string id) : id(id) {}
std::string id;
friend std::size_t hash( MyType const& self) {
return ::notstd::hash(self.id);
}
friend std::size_t hash( std::shared_ptr<MyType> const& self) {
if (!self) return 0;
return ::notstd::hash(*self);
}
};

which should give you a smart hash on on shared_ptr<MyType>.

This keeps the danger that someone changes id on a shared_ptr<MyType> which breaks every container containing the shared_ptr<MyType> in a non-local manner.

Shared state is the devil; consider writing a copy on write pointer if you are really worried about copying these things being expensive.



Related Topics



Leave a reply



Submit