Conditionally enable constructor in C++ class [duplicate]
Asked Answered
R

1

3

I am learning how to use std::enable_if and I have had some degree of success so far at conditionally enabling and disabling methods in my classes. I template the methods against a boolean, and the return type of such methods is an std::enable_if of such boolean. Minimal working example here:

#include <array>
#include <iostream>
#include <type_traits>

struct input {};
struct output {};

template <class io> struct is_input { static constexpr bool value = false; };

template <> struct is_input<input> { static constexpr bool value = true; };

template <class float_t, class io, size_t n> class Base {
private:
  std::array<float_t, n> x_{};

public:
  Base() = default;
  Base(std::array<float_t, n> x) : x_(std::move(x)) {}
  template <class... T> Base(T... list) : x_{static_cast<float_t>(list)...} {}

  // Disable the getter if it is an input class
  template <bool b = !is_input<io>::value>
  typename std::enable_if<b>::type get(std::array<float_t, n> &x) {
    x = x_;
    }

  // Disable the setter if it is an output class
  template <bool b = is_input<io>::value>
  typename std::enable_if<b>::type set(const std::array<float_t, n> &x) {
    x_ = x;
    }
};

int main() {
  Base<double, input, 5> A{1, 2, 3, 4, 5};
  Base<double, output, 3> B{3, 9, 27};

  std::array<double, 5> a{5, 6, 7, 8, 9};
  std::array<double, 3> b{1, 1, 1};

  // A.get(a);   Getter disabled for input class
  A.set(a);

  B.get(b);
  // B.set(b);   Setter disabled for input class

  return 0;
}

However, I cannot apply this procedure to conditionally enable constructors, since they have no return type. I have tried templating against the value of std::enable_if but the class fails to compile:

template <class io> class Base{
private:
  float_t x_;
public:
  template <class x = typename std::enable_if<std::is_equal<io,input>::value>::type> Base() : x_{5.55} {}
}

The compiler error looks like:

In instantiation of ‘struct Base<output>’ -- no type named ‘type’ in  ‘struct std::enable_if<false, void>’

As explained in this other post, when a class temnplate is instantiated, it instantiates all its member declarations (though not necessarily their definitions). The declaration of that constructor is ill-formed and hence the class cannot be instantiated.

How would you circumvent this issue? I Appreciate any help :)

EDIT:

I would like to have something like the following:

struct positive{};
struct negative{};

template <class float_t, class io> class Base{
private:
  float_t x_;
public:
  template <class T = io, typename = typename std::enable_if<
                          std::is_same<T, positive>::value>::type>
  Base(x) : x_(x) {}

  template <class T = io, typename = typename std::enable_if<
                          std::is_same<T, negative>::value>::type>
  Base(x) : x_(-x) {}

If that is possible at all.

Rickart answered 28/5, 2018 at 7:28 Comment(3)
Can the down-voter please explain the reason of his downvote?Mezzotint
Probably because a simple search finds a duplicate.Dakar
So why just don't close as duplicate?Mezzotint
D
5

There are 2 ways for constructors as you cannot use it in return type

Default template parameter:

template <class io> class Base
{
private:
    float_t x_;
public:
    template <class T = io,
              typename std::enable_if<std::is_equal<T, input>::value, int>::type = 0>
    Base() : x_{5.55} {}
};

Default argument:

template <class io> class Base{
private:
    float_t x_;
public:
    template <class T = io>
    Base(typename std::enable_if<std::is_equal<T, input>::value, int>::type = 0) :
        x_{5.55}
    {}
};

You cannot use SFINAE in your case as parameter depend only of your class parameter and not of template parameter of your function.

For:

template <class T = io, typename = typename std::enable_if<
                      std::is_same<T, positive>::value>::type>
Base(x) : x_(x) {}

template <class T = io, typename = typename std::enable_if<
                      std::is_same<T, negative>::value>::type>
Base(x) : x_(-x) {}

You do a common mistake: default template argument are not part of the signature, so you only declare and define twice

template <class, typename>
Base::Base(x);

That's why I used

typename std::enable_if<std::is_same<T, input>::value, int>::type = 0

where type is on left of =, so you will have 2 different type, so 2 different signatures.

C++20 adds requires which allow to discard method simply:

template <class float_t, class io> class Base{
private:
  float_t x_;
public:

  Base(x) requires(std::is_same<io, positive>::value) : x_(x) {}
  Base(x) requires(std::is_same<io, negative>::value) : x_(-x) {}

  // ...
};
Dakar answered 28/5, 2018 at 7:41 Comment(8)
I understand the problem that you point out. If the constructor was not templated against a class parameter, but against it own parameter, we could use #14603663. But this is not the case here. Is there any way around? I have edited my question to make it more clear. Thank you for your answer! :)Rickart
Edited to pinpoint your other problem in your edit.Dakar
That is some very valuable insight (for me) regarding default arguments and signature! But with the solution having int, the method is not disabled for std::enable_if<false,int>::type. It stills compiles perfectly fine. I am thinking now that maybe std::conditional is what I am lloking for, right?Rickart
std::enable_if<false,int>::type is an hard error, std::enable_if<cond<T>::value,int>::type with cond<T>::value resulting to false would disallow the overload. std::conditional doesn't seem to be what you want.Dakar
You are completly right! I was compiling a slightly different file than the one I posted here, where a different constructor was being called. And yeah, this is exactly what I needed. Thank you!Rickart
Did you perhaps meant to write std::is_same because there is no std::is_equal.Exedra
Thanks that's what I was looking for. Would you mind elaborating why you used T = io instead of using io directly?Exedra
SFINAE applies on template function and should be dependent on that template, if you use io directly, error comes from the class template parameter. C++20 adds a simple fix with Base() requires(std::is_equal<io, input>::value) : x_{5.55} {}.Dakar

© 2022 - 2024 — McMap. All rights reserved.