3

Recently I picked up the habit of typedefing various types within template classes, as is done in the standard library. For example, a container class might look something along the lines of:

template<typename T>
class custom_container {
public:
    typedef T value_type;
    typedef value_type* pointer;
    typedef value_type& reference;
    typedef std::size_t size_type;

    reference operator[](size_type);

    // etc...
};

However, after a while of using this, I began to question if it's really such a good idea, despite supposedly increasing readability and expression of intent.

It seems counter-intuitive that one would alias, say, T to another type, since isn't anyone using the class expecting the value_type to be T, and only ever T anyway (it is a custom_container<T> after all)? Similarly, would users of such a class not always expect pointer to be T* and reference to be T&?

We make use of typedefs to allow for ease of changing some aliased type to another if required, yet in the majority of the cases I come across, said typedefs are redundant, and almost confusing, as it would never make sense to have the alias synonymise any other type. custom_container probably wouldn't be fulfilling its expectations if value_type was changed to anything else than T - the user expects it is some sort of container of Ts.

Therefore, is it still useful and/or good design to make heavy use of typedefs in template classes as done in the standard library?

1
  • Note also that nobody forces you to use the T to begin with - it is just customary. You could as well start with template<typename value_type> and have value_type everywhere right away. – Aganju Aug 5 '17 at 1:35
2

These typedefs are useful for two reasons:

  • for abbreviating the names of very complicated types such as iterator types.

  • for writing robust generic code that makes use of your templated type and isn't aware of T. Pre C++11 you cannot write some generic code without using these typedefs, or at least not without cumbersome helper templates.

Both of these reasons are less necessary since C++11: decltype() can be used to find many types like the value type, and auto can be used to avoid spelling out complicated type names.

But both of these have limits.

  • decltype() can sometimes produce unexpected types, especially around constness, references, or when implicit conversions are involved. E.g. given a std::vector<bool> xs, the value_type is bool but decltype(xs[0]) would be some reference wrapper object, unless xs is const in which case it is a bool again. Accounting for that correctly (possibly via std::decay?) is very difficult.

  • Since auto will happily resolve to any type, it is not suitable when you do want to document and enforce a specific type. In some places like function parameters you cannot use auto (though this is already allowed by some compilers as an implicit function template declaration).

1

Those typedef's aren't only for you to use directly in normal code.

Yes, they might sometimes be more descriptive than specifying the type directly, or allow you to make sure you actually use the right type for the job.

Also, in C++11 auto and decltype took over that job for the most part, and it is far more convenient and harder to get wrong.

There are still edge-cases left though, for example when you have proxy-types like in std::vector<bool>.

And naturally, manually finding the right type might be a bit hampered when you are writing templates.

In the standard there is an additional reason for using those typedefs in the interface: Argument-names, template or function, are for exposition only.

0

isn't anyone using the class expecting the value_type to be T, and only ever T anyway?

Anyone using the class shouldn't be expecting that a T even exists, or that the class itself exists (maybe some other class will work better instead) :) Generic code will expect a container to fulfill some sort of a high-level concept, like Iterable. In particular, the code will:

  1. Not expect any particular container type (why would you limit it to custom_container?).

  2. Not expect that a container type is generic to begin with (it's not any sort of a requirement on a container to be generic!). You can have a class MyContainer { ... }; just fine.

  3. Not expect that a generic container's type is parametrized in any particular way. You can have a container type like std::array, except taking the size as the first parameter, the type second. Or you can have a container parametrized on things other than the value type, with the type fixed. You can have a container that extracts its value type from a std::tuple type. And so on: any sort of generic template-pattern-matching-based type extraction from container type itself (vs its members) will fail here.

Now, code that uses e.g. an Iterable concept, will be able to get at the value type, but look at what it takes - even today it's a mouthful:

template <typename Iterable> void foo(Iterable &iterable) {
  using value_type = std::decay_t<decltype(*iterable.begin())>;
  value_type value{};
  ...
}

vs.

template <typename Iterable> void foo(Iterable &iterable) {
  using value_type = Iterable::value_type;
  ...
}

The former is fine and dandy in modern C++ - once you do it correctly, as I expect my example to fail spectacularly when presented with some valid Iterable types, I'm sure. So it's not so trivial. You could also do auto value = *iterable.begin().

This would require a major template metaprogramming pain in C++03 - and that's where the requirement originated. It is indeed on the obsolete side today, but if you treat it so, then you should stick to implementing and using well documented concepts (in many cases, the concepts from the C++ standard will cover anything to do with containers).

0

It seems counter-intuitive that one would alias, say, T to another type, since isn't anyone using the class expecting the value_type to be T, and only ever T anyway (it is a custom_container<T> after all)? Similarly, would users of such a class not always expect pointer to be T* and reference to be T&?

These are most valuable when defining functions that work on generic types of containers.

Here's a simplified version of a bit of code that I have used at work.

template <typename ObjectMap>
struct KeyFilterFunctor
{
   typedef typename ObjectMap::value_type MapItem;
   typedef typename ObjectMap::key_type Key;

   EntityFilterFunctor(std::list<Key>& filteredKeys) : filtredKeys_(filteredKeys) {}

   void operator()(MapItem const& item)
   {
      // Assume <<SomePredicateFunction>> exists in scope.
      if ( <<SomePredicateFunction>>(item.second) )
      {
         filteredKeys_.push_back(item.first);
      }
   }

   std::list<Key>& filteredKeys_;
};

and used as

template <typename ObjectMap>
void getFilteredKeys(ObjectMap const& objectMap,
                     std::list<typename ObjectMap::key_type>& filteredKeys)
{
   KeyFilterFunctor<ObjectMap> entityFilter(filteredKeys);
   std::for_each(objectMap.begin(), objectMap.end(), entityFilter);
}

// Assume ObjectMap is defined as a type and
// objectMap is an object of type ObjectMap.
std::list<QString> filteredKeys;
getFilteredKeys(objectMap, filteredKeys);

IMO, presence of std::map::value_type and std::map::key_type has made the code easier to read and maintain.

I can imagine how you can write such generic functions that work with different kinds of containers, such as: std::vector, std::list, std::array, and std::set, without needing to know what the underlying container type is.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.