Унифицировать шаблоны С++ для указателей, значений и интеллектуальных указателей
Мой реальный пример довольно большой, поэтому я буду использовать упрощенный. Предположим, у меня есть тип данных для прямоугольника:
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';
}