Унифицировать шаблоны С++ для указателей, значений и интеллектуальных указателей

Мой реальный пример довольно большой, поэтому я буду использовать упрощенный. Предположим, у меня есть тип данных для прямоугольника:

struct Rectangle {
  int width;
  int height;

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

И другой тип, который использует этот тип, например:

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

Теперь я не хочу устанавливать ограничения на владельца для пользователей TwoRectangles, поэтому я хотел бы сделать его шаблоном:

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

Использование:

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

Проблема заключается в том, что если вызывающий пользователь хочет использовать указатели, тело функции должно быть другим:

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

Каков наилучший способ объединить мою templated функцию, чтобы сумма максимального количества повторялась для указателей, значений и умных указателей?

Ответ 1

Один из способов сделать это, инкапсулируя все в TwoRectangles, будет примерно таким:

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

Вряд ли у вас будет тип, для которого оба эти выражения действительны. Но вы всегда можете добавить дополнительную неоднозначность со вторым аргументом areaOf().


Другим способом было бы воспользоваться тем фактом, что уже есть способ в стандартной библиотеке вызова функции на что угодно: std::invoke(). Вам просто нужно знать базовый тип:

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;

и

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

Ответ 2

У меня на самом деле была аналогичная проблема некоторое время назад, в конце концов я решил не делать этого пока (потому что это большое изменение), но это породило решение, которое кажется правильным.

Я думал о создании вспомогательной функции для доступа к базовому значению, если есть какая-либо косвенность. В коде это будет выглядеть так же, как и с примером, похожим на ваш.

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