4

I would like to SFINAE-disable a class template constructor - but the catch is that an enable-disable condition is a trait of that class. Particularly, I would like to enable that constructor only if a specific class method, matching constructor's template arguments, exists in that class.

A simplified example:

struct S
{
    std::size_t n = 0;
    void set_value(int i) { n = i; }
    void set_value(const std::string & s) { n = s.size(); }
    template <class T, class = decltype(std::declval<S>().set_value(std::declval<T>()))>
    S(T && x)
    {
        set_value(std::forward<T>(x));
    }
};

That solution works in GCC and, allegedly, in MSVC, but does not work in Clang. And in my opinion Clang is in the right here, because inside a class definition class isn't yet defined, so std::declval<S>() should not work here.

So my idea was to move that constructor definition out of class definition:

struct S
{
    std::size_t n = 0;
    void set_value(int i) { n = i; }
    void set_value(const std::string & s) { n = s.size(); }
    template <class T, class>
    S(T &&);
};

template <class T, class = decltype(std::declval<S>().set_value(std::declval<T>()))>
S::S(T && x)
{
    set_value(std::forward<T>(x));
}

But that doesn't work in GCC and MSVC. That solution also looks somewhat fishy, because I first declare that constructor unconditionally and only later introduce SFINAE.

My question is - is it possible at all to solve that task in C++11/14/17? A specific task is that a class should have a constructor only if it also have a matching setter method. Obviously, one can manually define a non-template constructor for every setter method overload, but that tedious and hard to maintain.

But I'm also interested in more general task - SFINAE-enabling a constructor or other class method by a trait of that class (requiring a complete class definition).

Also, which compiler is more right in my example - GCC/MSVC or Clang?

songyuanyao
  • 169,198
  • 16
  • 310
  • 405

2 Answers2

4

You can introduce another template parameter with default value S, and use it for SFINAE. E.g.

template <class T, class X = S, class = decltype(std::declval<X>().set_value(std::declval<T>()))>
S(T && x)
{
    set_value(std::forward<T>(x));
}

LIVE

songyuanyao
  • 169,198
  • 16
  • 310
  • 405
  • IIRC, default template arguments are not considered for SFINAE. Am I wrong? – Daniel Langr Jan 14 '21 at 08:41
  • 2
    @DanielLangr The gotcha for default template arguments and SFINAE is that we may not provide two overloads which differ only in their default template arguments; SFINAE would, in theory, work for this but the program would be ill-formed as default template arguments are not part of function template's signature. – dfrib Jan 14 '21 at 08:46
  • @dfrib Thanks for the clarification, it makes sense. – Daniel Langr Jan 14 '21 at 08:47
  • 2
    @DanielLangr [Related Q&A](https://stackoverflow.com/questions/38305222/default-template-argument-when-using-stdenable-if-as-templ-param-why-ok-wit). – dfrib Jan 14 '21 at 08:47
1

For your question about which compiler is right, this is CWG1836

The problem is not std::declval: You can use std::declval<S> with incomplete S, since it returns a S&&. The issue is when you use std::declval<S>().set_value, since S is not complete at that moment.

However, there is an explicit exception:

the class type shall be complete unless the class member access appears in the definition of that class.

And the class member access does appear in the definition of S.

So gcc and msvc are right to allow it.


It seems like clang does not apply the resolution of CWG1836. In that case, it sees that std::declval<S>() does not have a dependent type, so it can immediately evaluate the member access std::declval<S>().set_value (and fails to do so).

To appease clang, you can make it a dependent type. For example, since you are already in a template, you can make it dependent on what T is:

template<typename T, typename...>
struct dependent_type_identity {
    using type = T;
};

template<typename T, typename... Dependence>
using dependent_type_identity_t = typename dependent_type_identity<T, Dependence...>::type;

struct S
{
    std::size_t n = 0;
    void set_value(int i) { n = i; }
    void set_value(const std::string & s) { n = s.size(); }
    template <class T, class = decltype(std::declval<dependent_type_identity_t<S, T>>().set_value(std::declval<T>()))>
    S(T && x)
    {
        set_value(std::forward<T>(x));
    }
};

Or do what this answer does, and make it dependent on another template argument which defaults to S (it is still a dependent type, even if there is no way to change what it is set to)

Artyer
  • 31,034
  • 3
  • 47
  • 75
  • Thanks for the link! Nice to know that it was properly addressed in the standard. Although, the solution with additional template argument is simpler, so I'll mark that as accepted. – Alexander Morozov Jan 18 '21 at 06:52