STiFU
STiFU

Reputation: 367

Conditionally hiding methods in a template depending on base type

I have a template that is derived from either std::vector or std::list. I need to hide all methods that enable altering the size of the underlying container. I know that I can hide methods via the using declaration. The problem is that some relevant methods are only available for std::list. Is it possible to conditionally hide those methods without explicitly overloading them (which would be a hassle)? Is there some fancy std::enable_if syntax that I can apply to the using declaration as well?

template <class ValueType, class BaseType = std::vector<ValueType>,
  std::enable_if_t<std::is_same_v<BaseType, std::vector<ValueType>> || std::is_same_v<BaseType, std::list<ValueType>>,int> = 0
>
struct Test : public BaseType
{
  Test() = default;

private:
  // Hide base members that are not supposed to be used directly
  using BaseType::push_back;
  using BaseType::insert;
  using BaseType::assign;
  using BaseType::emplace_back;
  using BaseType::resize;
  using BaseType::swap;

  // These methods must be hidden BaseType==std::list, but are not available for std::vector
  //using BaseType::merge;
  //using BaseType::push_front;
  //using BaseType::splice;
  //using BaseType::emplace_front; 
}

As a workaround, I could write new base class wrappers that are selected via std::conditional, like this, but it is surely not too nice:

template<class T>
struct CVector : public std::vector<T>
{
    using std::vector<T>::vector;
protected:
    using std::vector<T>::assign;
    using std::vector<T>::emplace_back;
    using std::vector<T>::insert;
    using std::vector<T>::push_back;
    using std::vector<T>::resize;
    using std::vector<T>::swap;
};

template <class T>
struct CList : public std::list<T>
{
    using std::list<T>::list;
protected:
    using std::list<T>::assign;
    using std::list<T>::emplace_back;
    using std::list<T>::insert;
    using std::list<T>::push_back;
    using std::list<T>::resize;
    using std::list<T>::swap;
    using std::list<T>::emplace_front;
    using std::list<T>::merge;
    using std::list<T>::push_front;
    using std::list<T>::splice;
};

template <class ValueType, class BaseType = std::vector<ValueType>,
  std::enable_if_t<std::is_same_v<BaseType, std::vector<ValueType>> || std::is_same_v<BaseType, std::list<ValueType>>,int> = 0
>
struct Test : public std::conditional<std::is_same_v<BaseType, std::vector<ValueType>>, CVector<ValueType>, CList<ValueType>>::type
{
  Test() = default;
}

Upvotes: 2

Views: 48

Answers (2)

Ted Lyngmo
Ted Lyngmo

Reputation: 117812

You could simplify it somewhat by making specializations of a base class template:

template<class>
struct Base; // not implemented for the types you don't support

template <class T, class Allocator>
struct Base<std::vector<T, Allocator>> : std::vector<T, Allocator> {
    using std::vector<T, Allocator>::vector;

   protected:
    using std::vector<T, Allocator>::assign;
    using std::vector<T, Allocator>::emplace_back;
    using std::vector<T, Allocator>::insert;
    using std::vector<T, Allocator>::push_back;
    using std::vector<T, Allocator>::resize;
    using std::vector<T, Allocator>::swap;
};

template <class T, class Allocator>
struct Base<std::list<T, Allocator>> : std::list<T, Allocator> {
    using std::list<T, Allocator>::list;

   protected:
    using std::list<T, Allocator>::assign;
    using std::list<T, Allocator>::emplace_back;
    using std::list<T, Allocator>::insert;
    using std::list<T, Allocator>::push_back;
    using std::list<T, Allocator>::resize;
    using std::list<T, Allocator>::swap;
    using std::list<T, Allocator>::emplace_front;
    using std::list<T, Allocator>::merge;
    using std::list<T, Allocator>::push_front;
    using std::list<T, Allocator>::splice;
};

The usage would then be simpler:

template<class T>
struct Test : Base<T> {
    Test() = default;
};

int main() {
    Test<std::vector<int>> v;
    Test<std::list<int>> l;
}

or if you wish:

template<class T, class BaseType = std::vector<T>>
struct Test : Base<BaseType> {
    Test() = default;
};

int main() {
    Test<int> v;
    Test<int, std::list<int>> l;
}

or if you don't want to repeat the value type when specifying the container:

template <class T, template <class, class> class BaseType = std::vector,
          class Allocator = std::allocator<T>>
struct Test : Base<BaseType<T, Allocator>> {
    Test() = default;
};

int main() {
    Test<int> v;
    Test<int, std::list> l; // note: `int` not needed
}

Upvotes: 1

Emad Kerhily
Emad Kerhily

Reputation: 280

The most robust and maintainable approach in standard C++ is indeed the workaround you've described. Why? Because using declarations are not templated, i.e. you cannot apply std::enable_if directly to a using declaration to conditionally include it. The presence of a using declaration is fixed once the class is instantiated.

By splitting the using declarations into CVector and CList, you isolate the container-specific method hiding into their own contexts. This leverages template specialization via std::conditional to choose the correct base, ensuring only valid using declarations are present.

Upvotes: 1

Related Questions