Concepts Lite vs enable_if

This post contains quite advanced material. I assume you are already familiar with Concepts Lite. For an overview of what Concepts Lite is, I recommend this proposal. Also, I have found this blog very useful regarding the details of and issues with concepts’ mechanics. One could look at Concepts Lite as three features:

  1. A superior alternative to enable_if (or overload hiding).
  2. The subsumption relation that enables the additional control of partial ordering in the overload resolution process.
  3. A convenient tool for building compile-time predicates that check for valid types and expressions.

In this post I will only focus on the first feature, and try to answer the question, “what do we need Concepts Lite for, given that we already have std::enable_if (and SFINAE)?”

The power of enable_if

Indeed, enable_if is quite powerful, and gives us the ability to define things like ‘conditionally explicit constructor’:

template <typename T>
class Wrapper
{
public:
  template <typename U, 
            ENABLE_IF(std::is_convertible<U, T>::value)>
    Wrapper (U && v);
   
  template <typename U, 
            ENABLE_IF(!std::is_convertible<U, T>::value)>
    explicit Wrapper (U && v);
};

I use a hand-crafted macro ENABLE_IF to make the examples focused, it is defined as:

# define ENABLE_IF(...) \
  typename std::enable_if<(__VA_ARGS__), bool>::type = true

Now, at this point, depending on how much you have been exposed to SFINAE-based tricks like this you may either respond “yes, this is how you declare such overloads,” or say “no, do you really expect me to read or write code like this?” Personally, I am so used to these tricks, that I find nothing difficult or unusual about them, but that may only prove how corrupt I have become. You can already see one reason why we want a dedicated feature: no normal human being should be forced to go through all these hacks and tricks.

I will skip the explanation of how it works exactly. The point I want to focus on is that this ENABLE_IF placed as additional template parameter can be thought of as saying “this template is invisible when the corresponding condition is not satisfied”. When I have two function templates with the opposite condition in ENABLE_IF, it can be thought of as saying “it is either this declaration or the other, depending on condition”. The two constructors (save for the condition) differ only by keyword explicit, so we could further say, “the constructor is either explicit or converting, depending on the condition.” This being ‘invisible’ is not exactly what we might expect in all circumstances, but is sufficient to guarantee the following:

void test(int i) 
{
  // int* -> void* (implicit)
  Wrapper<void*> w1 = &i; // ok
  Wrapper<void*> w2 {&i}; // ok

  // int* -> unique_ptr<int> (explicit)
  Wrapper<std::unique_ptr<int>> w3 = new int{}; // FAILS
  Wrapper<std::unique_ptr<int>> w4 {new int{}}; // ok
}

Thus, term ‘invisible’ works in the context of overload resolution. It also works in the context of testing “expression validity”:

using VPWrapper = Wrapper<void*>;
static_assert( is_convertible<int*, VPWrapper>::value, "");
static_assert( is_constructible<VPWrapper, int*>::value, "");

using UIWrapper = Wrapper<std::unique_ptr<int>>;
static_assert(!is_convertible<int*, UIWrapper>::value, "");
static_assert( is_constructible<UIWrapper, int*>::value, "");

Non-templates

enable_if solutions work well with templates, but sometimes we need to make a non-template function disappear. Of course, you never want to constrain a free-standing function:

int fun(int);

You either want it to exist or not: just decide. But the case is different for member functions in class templates. Suppose you want a wrapper class that is copy-constructible if and only if the wrapped type is copy-constructible. In other words, you want to implement a ‘conditional’ copy constructor. This boils down to the following task:

template <typename T>
class Wrapper
{
public:
  Wrapper (Wrapper const&); //< somehow enable iff 
                            //  is_copy_constructible<T>::value
};

This copy constructor is somehow a ‘templated entity’ but once the class template Wrapper is instantiated, in itself the constructor is a non-template. There is simply no way to employ SFINAE tricks to it, as any substitution has already taken place in the instantiation of class template Wrapper, and all declarations of non-template members have been already instantiated. Declarations — not definitions, so we have a declaration of a copy constructor for any T, no definition yet. It is enough to call a type copy-constructible, but instantiating the body of the copy constructor (when it is used) will trigger a hard compile-time error for non-copyable Ts.

This is where we can see the advantage of Concepts Lite constrains: they can be applied not only to templates, but also to non-template members of class templates:

template <typename T>
class Wrapper
{
public:
  Wrapper (Wrapper const&)
    requires std::is_copy_constructible<T>::value;
};

With this constraint imposed on a non-template function, the ability to disappear goes beyond substitution failure. The constructor declaration is not instantiated when we instantiate the class template.

Emulated non-templates

I was talking about a copy constructor because it is a special member function: it is never a template. If you declare a constructor template with the signature identical to that of the copy constructor, it is still not a copy constructor, and one will be generated by the compiler (even though it might be a worse match than your constructor template). More about this behavior in this post.

However, for any other member function that is not special, we can quite easily turn a non-template member function into a template. Imagine another Wrapper type:

template <typename T>
struct Wrapper
{
  T val;

  explicit Wrapper(T && v)
    : val(std::move(v)) {}

  explicit Wrapper(T const & v) // < disable for
    : val(v) {}                 //   non-copyable types
};

This is again a constructor, but this time it is not a special member function, so we can first turn it into a template:

template <typename T>
struct Wrapper
{
  template <typename U>
    explicit Wrapper(U const & v)
    : val(v) {} 
};

Now, obviously we allow too much compared to the original, so we have to constrain our template back so that the only allowed U is T:

template <typename T>
struct Wrapper
{
  template <typename U,
            ENABLE_IF(std::is_same<T, U>::value)>
    explicit Wrapper(U const & v)
    : val(v) {}
};

It looks a bit silly: we have practically arrived at the point we started from, but now we have a constructor template. Constraining it with becomes quite trivial: just add another predicate

template <typename T>
struct Wrapper
{
  template <typename U, 
            ENABLE_IF(std::is_same<T, U>::value &&
                      std::is_copy_constructible<U>::value)>
    explicit Wrapper(U const & v)
    : val(v) {}
};

And this practically does the job, convoluted syntax apart; but it has one downside.

Testing class templates

For a normal, non-template, function or class the very basic correctness test, performed at compile-time, is to check whether the piece of code in question even compiles. This checks (1) syntax correctness and (2) type-system correctness. In case of templates only (1) can be achieved immediately when the definition is visible: (2) cannot be performed due to the rules of two-phase name lookup: thus until we know what type our template is going to be instantiated with, we cannot validate the definition’s correctness concerning the type-system: even the obvious blunders. One very easy thing we can do to immediately test the minimum type-system correctness of a class template is to request an explicit class template instantiation: this instantiates definitions of all class’s non-template members. This is different than the implicit instantiation, which only instantiates the member functions’ declarations. This is very attractive: just type one instruction, and all the class’s member functions’ definitions are type-system-tested (for one type T). But it doesn’t work so well with conditional interfaces. If we go back to the previous definition of our Wrapper:

template <typename T>
struct Wrapper
{
  T val;

  explicit Wrapper(T && v)
    : val(std::move(v)) {}

  explicit Wrapper(T const & v) // < not disabled
    : val(v) {}                 //   in any way
};

Even though the second constructor’s definition has a type-system for T = std::unique_ptr<int>, we can safely perfotm an implicit instantiation of Wrapper<std::unique_ptr<int>>, because this only instantiates the constructor declarations, and the declaration alone is fine: you can take a std::unique_ptr<int> by a reference to const:

// ok: implicit instantiation
Wrapper<std::unique_ptr<int>> w {std::make_unique<int>(1)};

But the attempt to perform an explicit instantiation ends in failure:

// explicit instantiation:
template struct Wrapper<std::unique_ptr<int>>; // FAIL

This fails due to copy construction of T in the second constructor’s body. This problem is observable in std:vector and other STL containers.

Now, if we use the enable_if trick to conditionally disable the second constructor:

template <typename T>
struct Wrapper
{
  T val;

  explicit Wrapper(T && v)
    : val(std::move(v)) {}

  template <typename U, 
            ENABLE_IF(std::is_same<T, U>::value &&
                      std::is_copy_constructible<U>::value)>
    explicit Wrapper(U const & v)
    : val(v) {}
};

We will have fixed the compiler error:

// explicit instantiation:
template struct Wrapper<std::unique_ptr<int>>; // ok

But we have lost the opportunity to test the body of the second constructor, even if we test with a copy-constructible T:

template struct Wrapper<int>; // ok, but 2nd ctor's body
                              //     not instantiated

We get away with the compiler error, because the second constructor is a template, and member templates are simply skipped during the explicit class template instantiation.

Again, non-template member functions constrained with Concepts Lite do not have these problems. Consider the following definition:

template <typename T>
struct Wrapper
{
  T val;

  explicit Wrapper(T && v)
    : val(std::move(v)) {}

  explicit Wrapper(T const & v)
    requires std::is_copy_constructible<T>::value
    : val(v) {}
};

Now, the second constructor is not a template, so its body gets instantiated in:

template struct Wrapper<int>; // ok, both ctors' bodies tested

But because the second constructor’s constraints are not satisfied if T is not copy-constructible. Its instantiation is skipped in:

template struct Wrapper<std::unique_ptr<int>>; // ok

Yes: non-template member functions are not instantiated during the explicit template instantiation when their corresponding constraints are not satisfied. (However, you will not be able to verify it with GCC 6.0 because it did not implement the feature as per Concepts Lite TS.)

Summary

To wrap this up, Concepts Lite constrains are superior to SFINAE-based solutions in two ways. First, they are a dedicated feature providing a clear concise syntax, where intentions are clearly understood both by human programmers and by tools. Second, they work for non-template members of class templates, and unlike enable_if tricks which work by not selecting the overloads during overload resolution, they cause member function declarations never to be emitted in certain class template instantiations.

This entry was posted in programming and tagged , , , . Bookmark the permalink.

8 Responses to Concepts Lite vs enable_if

  1. Mihai Todor says:

    Regarding this piece of code:

    // ok: implicit instantiation
    Wrapper<std::unique_ptr> w {new int{1}};

    I believe it should be written like this:

    // ok: implicit instantiation
    Wrapper<std::unique_ptr> w {std::make_unique(1)};

    Otherwise, it refuses to compile under Clang / GCC.

    The reason I looked at it in detail is that I don’t really get why it works and the explanation you provided (“because this only instantiates the constructor declarations”) didn’t help me. Could you please elaborate? I have a feeling it has something to do with the fact that you can use unique_ptr with incomplete types (https://howardhinnant.github.io/incomplete.html), but I’m not sure…

    • Indeed, the code snippet should use make_unique! Thank you. I just fixed it. My apologies for the confusing example.

      Now, to the explanation. Let’s see the entire class again, but this time definitions provided out of class:

      template <typename T>
      struct Wrapper
      {
        T val;
        explicit Wrapper(T && v); 
        explicit Wrapper(T const & v);
      };
      
      template <typename T>
        Wrapper<T>::Wrapper(T && v) : val(std::move(v)) {}
      
      template <typename T>
        Wrapper<T>::Wrapper(T const & v) : val(v) {}
      

      Now, let’s try to emulate what implicit class template instantiation does: instantiate, the class, and member function declarations (but not definitions) for unique_ptr:

      template <typename unique_ptr<int>>
      struct Wrapper
      {
        T val;
        explicit Wrapper(unique_ptr<int> && v); 
        explicit Wrapper(unique_ptr<int> const & v); // ok
      };
      

      The second constructor’s declaration (no definition yet) is fine. There is nothing wrong with taking a unique_ptr by lvalue reference, provided that you are not attempting to copy it. But we don’t: in the function declaration we do not attempt to do anything.

      Now, let’s try to emulate what the explicit class template instantiation does: instantiate the class itself, the declarations and definitions of non-template members:

      template <typename unique_ptr<int>>
      struct Wrapper
      {
        T val;
        explicit Wrapper(unique_ptr<int> && v); 
        explicit Wrapper(unique_ptr<int> const & v); // ok
      };
      
      Wrapper<unique_ptr<int>>::Wrapper(unique_ptr<int> && v)
        : val(std::move(v)) {}
      
      Wrapper<unique_ptr<int>>::Wrapper(unique_ptr<int> const & v)
        : val(v) {} // << error here!
      

      Now, the declarations of constructors are fine again, as in the previous example, but in the instantiation of the second constructor we attempt to invoke a copy constructor of unique_ptr, which is illegal.

      Does this explanation help?

      • Mihai Todor says:

        Thanks Andrzej for confirming and fixing the issue!

        I think I didn’t phrase my question properly, though. I did see that the definition of the second constructor tries to make a copy of the unique_ptr which, of course, fails. What confuses me is the fact that the “implicit instantiation” version works. I was expecting that to fail as well, but now that I thought a bit more about it, in the case of implicit instantiation, the second constructor is not needed, so the compiler discards it during instantiation, right? I would like to better understand how implicit instantiation works.

        • To be more precise, during the implicit instantiation of a class template, compiler does instantiate declarations, but not definitions of all non-template member functions. (BTW, it may also instantiate the definitions of virtual member functions.) The reason for this is mainly compilation times. But also, because owing to this rule types like vector<unique_ptr<int>> work. Note that vector<unique_ptr<int>> is copyable (you can test it with type traits). But because the erroneous copy constructor body is never instantiated you can use such vector (provided you abstain from calling certain declared member functions).

        • Mihai Todor says:

          Yep, I understand now. Thank you very much for the details Andrzej!

  2. rdonch says:

    In the section “Emulated non-templates” your enable_if-based solution actually does more than disable the copy constructor when T is non-copyable, it also disallows passing in a subclass of T, e.g.:

    struct A {};
    struct B : A {};

    B b;
    Wrapper<A> w(b);

    Was this intentional? If not, wouldn’t it be better to put just ENABLE_IF(std::is_copy_constructible<T>::value) in the template parameter list?

    • My intention was to show how you can make a template work almost like a non-template. How identical is the former to the latter was not of my concern. But now that you ask, I think preventing sub-classes may be a positive feature: given that we will be storing an A inside, this prevents slicing.

      Dropping the is_same constraint would be counter to the idea I describe: you would change a constructor taking a specific type into a constructor taking about everything. If you want to retain the ability to bind to subclasses, I would rather change the constraint to:

      is_base_of<T, U>::value &&
      is_copy_constructible<T>::value
      

      or

      is_convertible<U, T>::value &&
      is_copy_constructible<T>::value
      
      • rdonch says:

        Sorry, I was unclear. I was proposing to remove the U template parameter and to have the constructor take a T const & v, like before.

        That said, I now realize that that doesn’t work, since the condition doesn’t depend on a template parameter for the constructor itself, and so is evaluated when the class is instantiated.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s