LINUX.ORG.RU

Дополнительная Специализация конструктора шаблонного класса

 , ,


0

2

Пробую (для обучения) реализовать свою двустороннюю очередь.

template <class T> class Deque
{
private:
    enum { defaultSize = 25, len = 5 };
    void dealloc ( );
    int alloc   ( const unsigned & = defaultSize, const T & = T() );
    T ** array;
    pair<int,int> xy;
...

public:
    Deque ( const unsigned &, const T & = T() );                       //1
    template <class Iterator> Deque (Iterator _first, Iterator _last); //2
    Deque ( ); 
    virtual ~Deque();


};

В main.cpp идёт создание дека:

Deque<int> test1(25, -1);

Все хорошо если нет 2-го конструктора. Второй конструктор «перебивает» действие первого. Почему так происходит я не понимаю, ведь в конструктор передаются типы const unsigned и const int и как я понимаю компилятору следовало бы направить этот путь к первому конструктору, но он упорно использует второй...

Подскажите пожалуйста что не так.

★★

разобрался

при передачи параметров - по-умолчанию используются signed величины...

Второй конструктор побеждает потому, что для первого требуется преобразование int -> unsigned int.

Так же нашел, что настоящих деках (stl, boost) - используется специальный тип, size_type, который (видимо) соответствует типу, которым, инстанцирован шаблон дека..

(хотя на самом деле он соответствует unsigned long) - тогда я понял почему не заходит в мой первый конструктор.. если объявить первый параметр так чтобы не требовалось преобразование типов - то в него успешно зайдет..

не понимаю тогда как схожей интерфейсной картине в stl и boost всё идёт как по маслу)

bonta ★★ ()

У тебя нет проверки Iterator на концепт итератора в темплейте, потому под аргумент может подойти любой тип. Посмотри как в реализациях stl в подобных случаях делают проверку (вырезка из libcxx):

    template <class _InputIter>
        deque(_InputIter __f, _InputIter __l,
              typename enable_if<__is_input_iterator<_InputIter>::value>::type* = nullptr);
Тут с помощью std::enable_if откидываются неправильные итераторы и через SFINAE выбирается правильный конструктор. Костылить вызов нужного констркутора через суффиксы нельзя. И таки зачем принимаешь unsigned int по ссылке?

mashina ★★★★★ ()
Ответ на: комментарий от mashina

Большое спасибо за ответ, узнал о новом, заюзал в своём коде, взятый из STL

template<typename _InIter>
    using _RequireInputIter = typename
        std::enable_if<std::is_convertible<typename
            std::iterator_traits<_InIter>::iterator_category,
                std::input_iterator_tag>::value>::type;

Получается в Си++ шаблоны могут использоваться не только при определении функций и классов, но и просто в выражениях? Этого я не знал..

Имею два важных вопроса :)

1. А вот на счет, ссылок - поясните пожалуйста или направьте почитать, я рукодствуюсь рекомедацией Мейерса - что нужно предпочитать передаче по значению передачу константного объекта по ссылке. Но похоже не все так просто..

Расскажите пожалуйста

2. Механизм понял, но enable_if судя по специализации и исходникам STL появляется только с Си++11, а как тогда код похожий на мой первый вариант (в начале темы) работал в Си++98? Возможно были определены нить дополнительные обёртки над конструкторами? Просто интересно очень, а нагуглить не получается.

Спасибо за хороший ответ.

bonta ★★ ()
Ответ на: комментарий от bonta

нужно предпочитать передаче по значению передачу константного объекта по ссылке

Ссылка занимает 4/8 байт. Поэтому большие объекты выгоднее передавать именно по ссылке. Примитивные типы и так занимают немного, так что разницы нет. Плюс теоретически будет лишняя косвенность. Подозреваю, что это всё равно оптимизируется, но так обычно не пишут.

а как тогда код похожий на мой первый вариант (в начале темы) работал в Си++98?

Для реализации enable_if не нужно ничего из С++11, то есть эта функция штука быть (и была) реализована и раньше, просто была не стандартной.

Если очень интересно - посмотри на старые исходники стл, но подозреваю, что там будет самопальный аналог.

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

Кстати, что это значит?

Deque<int> test1(25, -1);
Вот это ведь тоже «использование шаблона» и как раз в выражении.

DarkEld3r ★★★★★ ()
Последнее исправление: DarkEld3r (всего исправлений: 1)
Ответ на: комментарий от DarkEld3r

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

А stl попробую внимательнее посмотреть, но пока в нём не увидел, подобное решение - т.е. там как-будто бы нет remove_if (ну или не стандартизованного аналога), попробую посмотреть внимательнее.

Вот ещё вопросик:

Про размер типов понял, но сейчас глупый вопрос задам (в попытке расжевать самому себе), например есть ф-я a(T val) и ф-я z(T val) и между ними, допустим еще 30 ф-й с схожей сигнатурой и вот от а к b передается val... Ф-ии используют val только для чтения.

Не будет ли в данном случае передача по ссылке предпочтительнее?

Попробую сам ответить:

Если

T == int/char/double...

то тогда в стеке должно быть выделенно 32*(размер типа) байта памяти, и 32 раза вызван конструктор копирования POD (наконец-то узнал что значит POD тип) типа - который вероятно является просто операцией заноса байтов по определенном адресу (в стеке)

Если

T == int& /char& /double& ...

то тогда выделится 32*(размер ссылки) + такой же конструктор как выше но копирующий байтовое значение ссылки (а соответственно работающий несколько дольше, т.к. байт больше)

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

Правильно понимаю?

bonta ★★ ()
Ответ на: комментарий от bonta

Вот нашел хороший пример

#include <iostream>
 
struct Empty {};
struct Base { int a; };
struct Derived : Base { int b; };
struct Other { int a; int b; int c;};
 
int main()
{
    Empty e;
    Derived d;
    Base& b = d;
    Other o;
    Other &lo = o;
    int ib;
    int &lib = ib;
    std::cout << "size of empty class: "     << sizeof (e)          << '\n'
              << "size of pointer : "        << sizeof (&e)         << '\n'
              << "size of array of 10 int: " << sizeof(int[10])     << '\n'
              << "size of the Derived: "     << sizeof (d)          << '\n'
              << "size of the Derived through Base: " << sizeof b   << '\n'
              << "size of the Other: "       << sizeof (o)          << '\n'
              << "size of the Derived through Other: " << sizeof (lo)   << '\n'
              << "size of the int: "     << sizeof (ib)             << '\n'
              << "size of the link to int: "     << sizeof (lib)    << '\n';
 
}
size of empty class: 1
size of pointer : 8
size of array of 10 int: 40
size of the Derived: 8
size of the Derived through Base: 4
size of the Other: 12
size of the Derived through Other: 12
size of the int: 4
size of the link to int: 4

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

bonta ★★ ()
Ответ на: комментарий от bonta

Твой пример не работает, вернее не имеет смысла - sizeof(T &) - это сайзоф T, а не T &.

AnonCxx ()
Ответ на: комментарий от bonta

то тогда в стеке должно быть выделенно 32*(размер типа) байта памяти...

Это же зависит от архитектуры. На x86 будет передаваться через стек с выравниваним и кратностью размера 4 байтам. На amd64 простой тип полезет через регистры и может быть некоторые агрегаты, а сложные объекты могут всё равно передаваться по ссылке даже если в коде передаются по значению (посмотреть детали можно тут секция 3.2.3). Стека потребуется больше чем размер передаваемых через него данных, он используется и для технических нужд при вызове функций.

mashina ★★★★★ ()
Ответ на: комментарий от AnonCxx

Твой пример не работает, вернее не имеет смысла - sizeof(T &) - это сайзоф T, а не T &.

и правда, ссылка же «заменяется» оригинальной переменной.

В общем разобрался. И ведь знал же (понимал механику ссылки) что ссылка это своего рода указатель, который всегда разыменовывается, просто подзабыл... тогда всё становится очевидным и понятным, почему размер ссылки 4/8 (от архитектуры) и стало понятно где использование передачи по значению оправданно а где передачи по ссылке.

bonta ★★ ()
Ответ на: комментарий от mashina

оффтоп: никто не знает, когда завезут хоть какую-нибудь реализацию концептов в clang/gcc?

zamazan4ik ★★ ()
Ответ на: комментарий от zamazan4ik

Воспользуйся поисковиками, в gcc 6.1 уже какая-то реализация концептов имеется, но её поддержку нужно включать явно.

mashina ★★★★★ ()
Ответ на: комментарий от DarkEld3r

никому ненужное погружение в дебри STL по той же теме дека )

Для реализации enable_if не нужно ничего из С++11, то есть эта функция штука быть (и была) реализована и раньше, просто была не стандартной.

Покопал, и вот как там работает.. открываем /usr/include/c++/4.9/bits/stl_deque.h, и где-то в районе 899 строки будет:

#if __cplusplus >= 201103L
      deque(size_type __n, const value_type& __value,
            const allocator_type& __a = allocator_type())
      : _Base(__a, __n)
      { _M_fill_initialize(__value); }
#else
      explicit
      deque(size_type __n, const value_type& __value = value_type(),
            const allocator_type& __a = allocator_type())
      : _Base(__a, __n)
      { _M_fill_initialize(__value); } /*--------------- #1 */
#endif


#if __cplusplus >= 201103L
      template<typename _InputIterator,
               typename = std::_RequireInputIter<_InputIterator>>
        deque(_InputIterator __first, _InputIterator __last,
              const allocator_type& __a = allocator_type())
        : _Base(__a)
        { _M_initialize_dispatch(__first, __last, __false_type()); }
#else
      template<typename _InputIterator>
        deque(_InputIterator __first, _InputIterator __last,
              const allocator_type& __a = allocator_type())
        : _Base(__a)
        {
          // Check whether it's an integral type.  If so, it's not an iterator.
          typedef typename std::__is_integer<_InputIterator>::__type _Integral;
          _M_initialize_dispatch(__first, __last, _Integral());
        } /*--------------- #2 */
#endif

т.е. видно что в 11-м стандарте (как и описывалось в сообщениях выше) используется подход метапрограммирования и шаблон, который определяет «подключать» ли данный вызов - _RequireInputIter

В 98м же Си++ используется вызов на прямую, без таких вот конструкций, типа _RequireInputIter, как я и пытался сделать изначально, следуя определению интерфейса дека.

Но наткнулся на то, что (как и было выше мне объяснено) компилятор в общем случае воспринимает конструктор

template<typename _InputIterator>deque(_InputIterator _first, _InputIterator __last, const allocator_type& __a = allocator_type())
как более предпочтительный (т.к. из за наличия шаблонов, в него можно «всунуть» любой тип сразу, не задействуя преобразования типов) нежели конструктор
deque(size_type __n, const value_type& __value = value_type(), const allocator_type& __a = allocator_type())

Но и, собственно говоря, в реализации STL для си++98 все так и получается, как получалось у меня на момент написания первого сообщения в этой теме..

Такой вызов

std::deque<int> dq (10,-1)
идёт непосредственно к конструктору ----- #2

А вот такой

std::deque<int> dq (10u,-1)
, к конструктору ----- #1

Т.е. можно говорит, что до появления std::enable_if - конструктор ----- #1 почти не использовался, а лишь служил описанием интерфейса.

А его работу исполнял ----- #2, используя для своего «двойного» поведения метод

_M_initialize_dispatch(__first, __last, _Integral());

Который, в случае если вызов объекта _Integral() имеет тип __true_type - вызывает ф-ю _M_initialize_dispatch, которая, рассмартивает первые два параметра как простые типы - первый тип размера, второй тип заполняемого значения,

Если же при инстанцировании

__is_integer<InputIterator>::__type
InputIterator не попадает ни в одну из перегруженных для __is_integer специализаций, то объект _Integral() имеет тип __false_type

И в такмо случае используется перегруженная версия

_M_initialize_dispatch
, которая, переданные в неё параметры воспринимает как итераторы а не базовые типы.

И вот, таким вот образом, один конструктор, (с другим интрефейсом и как бы созданный совсем для другого) эмулирует действия первого

Убедиться в этом можно, заккоментив stl_deque.h пару строк

      template<typename _Integer>
        void
        _M_initialize_dispatch(_Integer __n, _Integer __x, __true_type)
        {
	  //_M_initialize_map(static_cast<size_type>(__n));
	  //_M_fill_initialize(__x);
	}

и выполнив код

std::deque<int> dq (10,-1);
std::copy ( dq.begin(), dq.end(), std::ostream_iterator<int>(std::cout, " "));
собранный для -std=c++11 и -std=c++98

bonta ★★ ()

Да, я понимаю что n-му кол-ву людей это все очевидно, а кому-то не очевидно, но и даже если и не очевидно, как работает это изнутри знать не обязательно.

Но просто интересно, в том числе и для изучения ООП/Си++ имхо, к тому же через некоторое время тонкости работы этого дела могут забыться, а так, взлянешь на свою собственнюу темку, через некоторое время, и с чувством глубокого удовлетворения от того что понимаешь как оно работает (или работало в исторической ретроспективе ранее), пойдешь заниматься полезными делами, а это знание возможно и пригодится когда-нибудь при проектировании чего-нибудь в качестве шаблона или антишаблона )

bonta ★★ ()
Ответ на: комментарий от bonta

Не оправдывайся - всё правильно сделал, что запостил результат «исследования» сюда. Может действительно кто ещё вопросом задастся и эту тему найдёт.

DarkEld3r ★★★★★ ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.