简体   繁体   中英

Unify C++ templates for pointers, values and smart pointers

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?

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);
  }
};

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';
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM