2017-01-31 2 views
12

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

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

Usages:

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

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

+7

Вы можете написать кучу перегрузок с помощью SFINAE, чтобы исключить неприменимые. Или вы можете изменить свой дизайн, чтобы иметь дело только с теми вещами, которые вам нужны, и не оставлять вещи, которые, как вы себе представляете, вам понадобятся, пока вы им не понадобятся. –

+4

Используйте только '->'. Обменивайте значения без указателя в пользовательские объекты, подобные указателям. –

+1

Вы можете сделать то, что стандартная библиотека, и дать ему дополнительный тип шаблона, который является признаком, который знает «как взять' T' и вызвать 'computeArea' на нем». Или просто не переусердствуйте. :) – GManNickG

ответ

21

Один из способов сделать это, герметизирующего все в 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'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'; 
} 
+1

так что ваш нисходящий? скажите мне, что мне не хватает, я был бы рад узнать – Sopel

+1

Есть несколько проблем с этим. 1) Перегрузки 'T *' и 'T const *' избыточны. 2) Перегрузка 'T const &' избыточна и опасна с перегрузкой 'T & ', потому что, если вы передаете rvalue, вы получаете оборванную ссылку 3) Это не поддерживает интеллектуальные указатели. – Barry

+0

@Barry спасибо.Хотя я пробовал без const & overload, но тогда он не работал с константными переменными (возможно, я делал что-то неправильно?), Что, я думаю, очень важно. И хотя я согласен, что это опасно, потому что rvalues ​​можно передать, вся идея об изменении семантики, как это, опасна. Я оставил его. Я внес изменения в код, теперь он должен обрабатывать интеллектуальные указатели. Это должно быть возможно лучше, но сейчас у меня мало времени. – Sopel

Смежные вопросы