9

I have made a type-safe ID class, but now I want to support operator++ if the underlying type also has it. From this and this answears I have come up with 2 alternatives, but they both fail when instantiated with AId:

template<typename T, typename TID = unsigned int>
struct AId {
  typedef AId<T, TID> type;
  typedef T handled_type;
  typedef TID value_type;

private:
  value_type id;

  template<typename _T> struct IsIncrementable
  {
    template<typename _U> using rm_ref = typename std::remove_reference<_U>::type;
    typedef char (&yes)[1];
    typedef char (&no)[2];
    template<class _U>
    static yes test(_U *data, typename std::enable_if<
                      std::is_same<_U, rm_ref<decltype(++(*data))>>::value
                    >::type * = 0);
    static no test(...);
    static const bool value = sizeof(yes) == sizeof(test((rm_ref<_T> *)0));
  };

public:
  explicit AId(const value_type &id) : id(id) {}

...

  //This fails with error: no match for 'operator++' (operand type is
  //'AId<some_type, std::basic_string<char> >::value_type
  //{aka std::basic_string<char>}')
  //auto operator++() -> decltype(++id, std::ref(type())) { ++id; return *this; }
  //                              ^
  template<typename = decltype(++id)>
  auto operator++() -> decltype(++id, std::ref(type())) { ++id; return *this; }

  //error: no type named 'type' in 'struct std::enable_if<false, int>'
  template<typename std::enable_if<IsIncrementable<value_type>::value, int>::type = 0>
  type operator++(int /*postfix*/) { type old(id); ++id; return old; }
};

How can AId<> have operator++ only if AId<>::value_type also has it? I'm limited to c++11 and no boost.

Previously there was a second question from which I have made a question on its own here.

Although I'm actually using @Sam Varshavchik answear in my code, I consider the one provided by @Guillaume Racicot to be more general, so I chose that as a solution.

I'm now using @Barry's answear which is both simple and correct.

Community
  • 1
  • 1
Olivetree
  • 424
  • 3
  • 11

2 Answers2

7

I see no need for SFINAE, or no need for anything at all.

Just implement your operator++. If the underlying class does not support it, and operator++ is not invoked for your template wrapper, the operator does not get instantiated, no harm no foul.

Tested with gcc 6.2.1, note that mytemplate<not_incrementable> will instantiate with no issues, as long as nothing tries to increment it:

#include <iostream>
#include <vector>

class not_incrementable {};

class incrementable {
public:

    incrementable &operator++()
    {
        return *this;
    }
};

template<typename T> class mytemplate {

public:

    T t;

    mytemplate<T> operator++()
    {
        ++t;
        return *this;
    }
};

void foo()
{
    mytemplate<not_incrementable> will_this_compile;

    mytemplate<incrementable> will_this_compile2;

    ++will_this_compile2; // Compiles

    // ++will_this_compile; // Will not compile
}
Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148
  • 2
    So template classes can be incomplete without a problem? :O I always thought that when a template class is instantiated all non-template member function must be compilable and exist statically, just like a regular class. Could you give a reference which states why your code should compile on any standard C++ compiler (and not just g++)? – Olivetree Oct 12 '16 at 13:16
  • Actually just the opposite, methods of template classes are instantiated iff they are called; this is actually required. I don't have a standard quote, but consider std::vector. What does the copy constructor look like? – Nir Friedman Oct 12 '16 at 13:43
  • @NirFriedman Indeed, I had never realized that particular case. But then why does std::vector need items to be default-constructable? It could just avoid implementing resize(), and allocate just the memory, without initializing elements. Without default-initialization it would even be faster to reserve(). – Olivetree Oct 12 '16 at 14:05
  • @Olivetree In C++14 or 17, many of the "you must have property X" requirements on objects in std containers where relaxed and replaced with per-operation requirements. Sometimes the requirements for a given operation may surprise you, however, like push back requiring move constructor even though you ensured it had enough space with a previous call to reserve. – Yakk - Adam Nevraumont Oct 12 '16 at 14:09
  • An even better example: `std::shared_ptr`. How the heck are you supposed to instantiate that thing's `operator*`? – Sam Varshavchik Oct 12 '16 at 15:07
4

As stated in other answer, indeed, you can left function that have errors not instantiated if you don't use them.

However, if someone use sfinae to try to check if your class supports operator++, his type trait will give him false positive, causing potential compilation errors. If you want to support that use case, you are left with not a lot of choices. You need to implement it conditionally.

If you want conditional implementation of a member, you can use inheritance.

We will put sfinae in that trait and use that trait after:

template<typename, typename>
struct has_increment : std::false_type {};

template<typename T>
struct has_increment<T, void_t<decltype(++std::declval<T>())>> : std::true_type {};

The type void_t can be implemented like this (with C++11 compatibility):

// void_t implemented with a struct works better for c++11 compilers
template<typename...>
struct voider { using type = void; };

template<typename... Ts>
using void_t = typename voider<Ts...>::type;

Now, we can implement the operator++ of your class in a mixin:

template<typename Child>
struct MixinIncrement {
    auto operator++(int /*postfix*/) {
        Child::type old(self().id);
        ++(self().id);
        return old;
    }

private:
    const Child& self() const { return *static_cast<const Child*>(this); }
    Child& self() { return *static_cast<Child*>(this); }
};

Now, to conditionally implement the operator++ function, you can use std::conditional with our trait:

struct Dummy {};

template<typename Child>
using Parent = typename std::conditional<has_increment<TID>::value, MixinIncrement<Child>, Dummy>::type;

template<typename T, typename TID = unsigned int>
struct AId : Parent<AId<T, TID>> {
    /* your stuff */
};

Now, since you extends the mixin only if the type is matching the type trait, you only get the operator++ if TID has the increment operator. You end up extending Dummy if not, which don't have the operator implemented.

This very same trick is nice if you want to conditionally implement copy and move constructors.

Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141
  • Nice answear, but this requires much more code than @Sam Varshavchik's. Is there any reason this one is preferrable? I suppose this actually uses SFINAE, but it's not a requirement, I just thought it was the only solution. Also, regarding your definition of void_t, acoording to [this](http://en.cppreference.com/w/cpp/types/void_t) it should actually be `template struct make_void { typedef void type;};` `template using void_t = typename make_void::type;` – Olivetree Oct 12 '16 at 13:42
  • See my edit. I added a reason in the first paragraph – Guillaume Racicot Oct 12 '16 at 13:43
  • The name `voider` or `make_void` has no impact. This is not part of any standard and the name `make_void` was chosen "like that" in the example. Fell free to change the name if it. – Guillaume Racicot Oct 12 '16 at 13:45