Search code examples
c++templatesc++11c-preprocessorgeneric-programming

Is a templated function that supports different levels of indirection possible in C++11?


Essentially, I'm curious if you can use C++11 templates to make it so a templated function can detect an iterator's level of indirection and compile the function differently depending on that. For example, here is some code that won't compile:

#include <vector>
#include <list>
#include <type_traits>
#include <iostream>

struct MyStruct
{
  int value;
  MyStruct(int value = 42) : value(value) { }
  const int& getInt() const { return value; }
};

typedef std::list<MyStruct> StructList;
typedef std::vector<const MyStruct*> StructPtrVector;

template <typename ITER_TYPE>
const int& getIteratorInt(ITER_TYPE iter)
{
  if (std::is_pointer<decltype(*iter)>::value)
    return (*iter)->getInt(); // Won't compile -> MyStruct has no operator-> defined
  return iter->getInt(); // Won't compile -> MyStruct* has the wrong level of indirection
}

template <typename LIST_TYPE>
void PrintInts(const LIST_TYPE& intList)
{
  for (auto iter = intList.begin(); iter != intList.end(); ++iter)
    std::cout << getIteratorInt(iter) << std::endl;
}

int main(void)
{
  StructList structList;
  StructPtrVector structPtrs;
  int values[5] = { 1, 4, 6, 4, 1 };
  for (unsigned i = 0; i < 5; ++i)
  {
    structList.push_back(values[i]);
    structPtrs.push_back(&structList.back());
  }
  PrintInts(structList);
  PrintInts(structPtrs);

  return 0;
}

The obvious situation is when you have a list of objects, and then a different kind of list of pointers to objects. And, what you want to do is the same to both lists, by treating them both as lists of objects.

The above code won't compile, because it is doing a logical check that should be done at compile-time. I don't know if there is a way to do this with preprocessor macros. I tried a simple #if std::is_pointer<decltype(*iter)>::value == true, but the compiler seems to always consider it false. (I've never tried preprocessor macros much before, but that is clearly not the proper way.)

Any idea if it's even possible?


Solution

  • When you want to select between two implementations depending on a metafunction such as is_pointer, use std::enable_if, which works on the principle of SFINAE.

    template <typename ITER_TYPE>
    auto getIteratorInt(ITER_TYPE iter) ->
    typename std::enable_if< std::is_pointer< 
            typename std::iterator_traits< ITER_TYPE >::value_type >::value,
        const int& >::type
    {
      return (*iter)->getInt();
    }
    
    template <typename ITER_TYPE>
    auto getIteratorInt(ITER_TYPE iter) ->
    typename std::enable_if< ! std::is_pointer< 
            typename std::iterator_traits< ITER_TYPE >::value_type >::value,
        const int& >::type
    {
      return iter->getInt();
    }
    

    This way, template instantiation only sees lines of code that are valid for the given template arguments.

    I've just applied this fix to your code mechanistically… I'm not advocating the use of multiple indirection or suggesting that the fixed implementation is robust.