Filipe Rodrigues
Filipe Rodrigues

Reputation: 2177

Delete templated struct partial specialization

If I have a templated struct as such:

template<typename T>
struct A {};

How can I remove the partial specialization for a certain type, such as void so that any mention of A< void > will give a compiler error?

template<>
struct A<void> = delete;

This doesn't compile as is expected as this syntax doesn't exist, although I'd essentially want something similar to this.

Although I do currently have a 'solution' to this problem, which is to delete all possible constructors:

template<>
struct A<void> { template<typename...> A(...) = delete; };

But this isn't the best solution as the user can still use A< void > as long as they don't try to instantiate an object. I've also tried to add an enable_if to the initial structure as such:

template<typename T, typename = typename std::enable_if<!std::is_same<T, void>::value>::type>
struct A {};

This works well, it makes it so the mention of A< void > gives a compiler error (Although somewhat criptic, since it refers to the enable_if itself), but this doesn't work if I don't have access to the initial definition such as when specializing someone else's structure.

So is there any good way to do this aside from the solution I have so far, and if not, is there a way to improve the one I have to give a compiler error when the user uses A< void >?

I can use c++17 and clang's c++2a if it adds any new features that would help with this.


Update: As @PicaudVincent said in his answer, the second solution wouldn't allow you to restrict all types based on a condition, only single types, but there is a way around it:

We could write a perfect wrapper around our struct A, that is, a wrapper that behaves exactly like the original type with the help of a Helper function

template<template<typename...> class U, typename=void, typename...TArgs>
struct Helper : U<TArgs...>
{
    using U<TArgs...>::U;

    template<typename...Args>
    Helper(Args...args) : U<TArgs...>(args...) {};
};

template<typename...Args>
using B = Helper<A,
                 typename std::enable_if<(... && std::is_arithmetic<Args>::value)>::type, 
                 Args...>;

And now you can use B<> instead of A<>, even though they are the same thing and will behave in the same way and will get converted to A<> whenever needed, such as when calling a function that takes in A<>.

Although if a function takes in B<>, you cannot pass A<> unless you explicitly convert the A<> to B<> with static_cast or similar.

This also works for any templated class, you have just to substitute A for the class you're trying to use and the enable_if with your condition as such:

using B = Helper</*Structure to use*/,
                 typename std::enable_if</*Condition*/>::type, 
                 Args...>;

This isn't the best solution, but it does allow you to now have to change the original declaration and still restrict what you can put in there without having to name each type manually.

Upvotes: 3

Views: 996

Answers (2)

Filipe Rodrigues
Filipe Rodrigues

Reputation: 2177

In the end, there are 3 methods that can be done to accomplish this, each with their ups and downs:

1. [@PicaudVincent's suggestion]

In the initial body of the structure, write a static_assert with the conditions

template<typename T>
struct A
{
    static_assert(!std::is_same<T, void>::value);
    ...
}

Pros:

  • Easy to add new conditions
  • Any condition can be added
  • Helpful error messages generated

Cons:

  • Requires access to the original struct

2.

Define the specialization without a body, thus preventing the compiler from being able to use that specialization

template<>
struct A<void>;

Will give an error message as such when trying to instantiate the object:

aggregate 'A<void> Var' has incomplete type and cannot be defined

Pros:

  • Doesn't require access to the original struct

Cons:

  • Only allows to specialize on 1 type
  • The user can define a body afterwards and remove this deletion

3

You can write a helper class which essentially wraps the type you want to specialize into a new type with all the functionality that you not have control over, so you can use method 1 to customize it or even just std::enable_if.

template<template<typename...> class U, typename=void, typename...TArgs>
struct Helper : U<TArgs...>
{
    using U<TArgs...>::U;
    
    template<typename...Args>
    Helper(Args...args) : U<TArgs...>(args...) {};
    
    static_assert(/*Condition*/);
};

template<typename...Args>
using B = Helper</*Type to wrap around*/,
                 typename std::enable_if</*Condition*/>::type, 
                 Args...>;

Pros:

  • Doesn't require the defining body and allows for more control over the class
  • Easy to add new conditions
  • Any condition can be added
  • Helpful error messages with static_assert
  • Type B works just like A, since it can be converted easily

Cons:

  • Wraps the type and so you have to switch all declarations of A to B
  • Functions that take in a B cannot take in an A, so most functions should be declared with A, which would mean keeping track of 2 types instead of 1 (unless purely B is used).

Upvotes: 1

Picaud Vincent
Picaud Vincent

Reputation: 10982

If you really want a "compile-time" error you can use static_assert. Code to compile with --std=c++14 as follows:

#include <type_traits>

template <typename T>
struct A
{
  static_assert(!std::is_same<T, void>::value, "Not allowed");
};


int main()
{
  A<double> a_d;
  A<void> a_v;     // <- compile time error
}

Update:

I understand your comment. Unfortunately something like:

template <>
struct A<void>
{
  static_assert(false, "Not allowed");
};

does not work, as the condition for sure is false and compiler detects that.

Not sure we can find a static_assert based solution in that case. On my side I have not found one for the moment.


Update 2: Filipe Rodrigues self-answer

Alternative, simply define struct A<void> without body.

template<> struct A<void>; 

then code like:

int main()
{
  A<double> a_d;
  A<void> a_v;
}

triggers this kind of error:

aggregate ‘A<void> a_v’ has incomplete type and cannot be defined

1/ versus 2/

2/ has the advantage of simplicity, however with 1/ you can write:

template <typename T>
struct A
{
  static_assert(std::is_arithmetic<T>::value, 
                "A<T>, T: must be an arithmetic type");
};

which is something you can not do with 2/

Upvotes: 2

Related Questions