12

My real example is quite big, so I will use a simplified one. Suppose I have a data-type for a rectangle:

struct Rectangle {
  int width;
  int height;

  int computeArea() {
    return width * height;
  }
}

And another type that consumes that type, for example:

struct TwoRectangles {
  Rectangle a;
  Rectangle b;
  int computeArea() {
    // Ignore case where they overlap for the sake of argument!
    return a.computeArea() + b.computeArea();
  }
};

Now, I don't want to put ownership constraints on users of TwoRectangles, so I would like to make it a template:

template<typename T>
struct TwoRectangles {
  T a;
  T b;
  int computeArea() {
    // Ignore case where they overlap for the sake of argument! 
    return a.computeArea() + b.computeArea();
  }
};

Usages:

TwoRectangles<Rectangle> x;
TwoRectangles<Rectangle*> y;
TwoRectangles<std::shared_ptr<Rectangle>> z;
// etc... 

The problem is that if the caller wants to use pointers, the body of the function should be different:

template<typename T>
struct TwoRectangles {
  T a;
  T b;
  int computeArea() {
    assert(a && b);
    return a->computeArea() + b->computeArea();
  }
};

What is the best way of unifying my templated function so that the maxiumum amount of code is reused for pointers, values and smart pointers?

sdgfsdh
  • 33,689
  • 26
  • 132
  • 245
  • 7
    You can write a bunch of overloads with SFINAE to rule out inapplicable ones. Or you can change your design to deal with only the things you need, and leave out things you imagine you might need until you actually need them. – Pete Becker Jan 31 '17 at 15:42
  • 4
    Use only `->`. Wrap non-pointer values in custom pointer-like objects. – n. m. could be an AI Jan 31 '17 at 15:44
  • 1
    You could do what the standard library and give it an additional template type, which is a trait that knows "how to take the `T` and call `computeArea` on it". Or just not over-engineer it. :) – GManNickG Jan 31 '17 at 15:46

2 Answers2

22

One way of doing this, encapsulating everything within TwoRectangles, would be something like:

template<typename T>
struct TwoRectangles {
  T a;
  T b;

  int computeArea() {
    return areaOf(a) + areaOf(b);
  }

private:
    template <class U>
    auto areaOf(U& v) -> decltype(v->computeArea()) {
        return v->computeArea();
    }

    template <class U>
    auto areaOf(U& v) -> decltype(v.computeArea()) {
        return v.computeArea();
    }
};

It's unlikely you'll have a type for which both of those expressions are valid. But you can always add additional disambiguation with a second argument to areaOf().


Another way, would be to take advantage of the fact that there already is a way in the standard library of invoking a function on whatever: std::invoke(). You just need to know the underlying type:

template <class T, class = void>
struct element_type {
    using type = T;
};

template <class T>
struct element_type<T, void_t<typename std::pointer_traits<T>::element_type>> {
    using type = typename std::pointer_traits<T>::element_type;
};

template <class T>
using element_type_t = typename element_type<T>::type;

and

template<typename T>
struct TwoRectangles {
  T a;
  T b;

  int computeArea() {
    using U = element_type_t<T>;
    return std::invoke(&U::computeArea, a) + 
        std::invoke(&U::computeArea, b);
  }
};
Barry
  • 286,269
  • 29
  • 621
  • 977
2

I actually had a similar problem some time ago, eventually i opted not to do it for now (because it's a big change), but it spawned a solution that seems to be correct.

I thought about making a helper function to access underlying value if there is any indirection. In code it would look like this, also with an example similar to yours.

#include <iostream>
#include <string>
#include <memory>

namespace detail
{
    //for some reason the call for int* is ambiguous in newer standard (C++14?) when the function takes no parameters. That's a dirty workaround but it works...
    template <class T, class SFINAE = decltype(*std::declval<T>())>
    constexpr bool is_indirection(bool)
    {
        return true;
    }
    template <class T>
    constexpr bool is_indirection(...)
    {
        return false;
    }
}
template <class T>
constexpr bool is_indirection()
{
    return detail::is_indirection<T>(true);
}

template <class T, bool ind = is_indirection<T>()>
struct underlying_type
{
    using type = T;
};

template <class T>
struct underlying_type<T, true>
{
    using type = typename std::remove_reference<decltype(*(std::declval<T>()))>::type;
};

template <class T>
typename std::enable_if<is_indirection<T>(), typename std::add_lvalue_reference<typename underlying_type<T>::type>::type>::type underlying_value(T&& val)
{
    return *std::forward<T>(val);
}

template <class T>
typename std::enable_if<!is_indirection<T>(), T&>::type underlying_value(T& val)
{
    return val;
}
template <class T>
typename std::enable_if<!is_indirection<T>(), const T&>::type underlying_value(const T& val)
{
    return val;
}


template <class T>
class Storage
{
public:
    T val;
    void print()
    {
        std::cout << underlying_value(val) << '\n';
    }
};

template <class T>
class StringStorage
{
public:
    T str;
    void printSize()
    {
        std::cout << underlying_value(str).size() << '\n';
    }
};

int main()
{
    int* a = new int(213);
    std::string str = "some string";
    std::shared_ptr<std::string> strPtr = std::make_shared<std::string>(str);
    Storage<int> sVal{ 1 };
    Storage<int*> sPtr{ a };
    Storage<std::string> sStrVal{ str };
    Storage<std::shared_ptr<std::string>> sStrPtr{ strPtr };
    StringStorage<std::string> ssStrVal{ str };
    StringStorage<const std::shared_ptr<std::string>> ssStrPtr{ strPtr };

    sVal.print();
    sPtr.print();
    sStrVal.print();
    sStrPtr.print();
    ssStrVal.printSize();
    ssStrPtr.printSize();

    std::cout << is_indirection<int*>() << '\n';
    std::cout << is_indirection<int>() << '\n';
    std::cout << is_indirection<std::shared_ptr<int>>() << '\n';
    std::cout << is_indirection<std::string>() << '\n';
    std::cout << is_indirection<std::unique_ptr<std::string>>() << '\n';
}
Sopel
  • 1,179
  • 1
  • 10
  • 15
  • 1
    so thy the downvote? tell me what i'm missing, i would be glad to learn – Sopel Jan 31 '17 at 17:06
  • 1
    There are a few problems with this. 1) The `T*` and `T const*` overloads are redundant. 2) The `T const&` overload is redundant and dangerous with the `T&` overload because if you pass an rvalue in you get a dangling reference out 3) This doesn't support smart pointers. – Barry Jan 31 '17 at 18:57
  • @Barry thanks. Though I have tried without const& overload but then it wasn't working with const variables (maybe i was doing something wrong?), which I think is very important. And while I agree that it is dangerous, because rvalues can be passed, the whole idea about changing semantics like that is dangerous. I have left it. I have made alterations to the code, now it should handle smart pointers. It should be possible to it better but I don't have much time right now. – Sopel Jan 31 '17 at 22:39
  • also underlying_type may not be the best name, since it is used in standard library for enum types... – Sopel Jan 31 '17 at 22:47
  • @Barry Ok. I tested it on msvc and it worked. Seems to not work on GCC though... will investigate – Sopel Feb 01 '17 at 18:24
  • @Barry fixed it, but it's not the prettiest. – Sopel Feb 01 '17 at 18:43