На главную Наши проекты:
Журнал   ·   Discuz!ML   ·   Wiki   ·   DRKB   ·   Помощь проекту
ПРАВИЛА FAQ Помощь Участники Календарь Избранное RSS
msm.ru
Модераторы: Qraizer, Hsilgos
  
> Шаблонный конструктор в нешаблонном классе
    Всем привет!

    Решил я немного пометапрограммировать, вернее попытаться ... но произошел горкий катаклизьм! :lol:

    Суть в следующем. Делаю тестовую программу main.cpp:

    ExpandedWrap disabled
      #include <iostream>
      #include "anydigit.h"
       
      using namespace std;
       
      int main() {
        AnyDigit any(3.14);
        cout << "Any: " << any << endl;
        return 0;
      }

    А в anydigit.h пишу следующее:

    ExpandedWrap disabled
      #ifndef ANYDIGIT_H
      #define ANYDIGIT_H
       
      #include <ostream>
       
      class AnyDigit {
          double _value;
        public:
          AnyDigit(): AnyDigit(0.0) {};
       
          template<typename T> AnyDigit(const T value) {
            static_assert(
              (std::is_integral_v<T> || std::is_floating_point<T>::value),
              "Желаю аргумент в виде целого или числа с плавающей!"
            );
            _value = value;
          };
       
          friend std::ostream& operator<<(std::ostream& os, const AnyDigit& obj) {
            os << std::scientific << obj._value;
            return os;
          };
      };
       
      #endif  // ANYDIGIT_H

    И все собирается, и запускается, и работает. Но как только я хочу в заголовочном файле оставить только объявление:

    ExpandedWrap disabled
      template<typename T> AnyDigit(const T value);

    А реализацию перенести в .cpp файл:

    ExpandedWrap disabled
      #include "anydigit.h"
       
      template<typename T> AnyDigit::AnyDigit(const T value) {
        static_assert(
          (std::is_integral_v<T> || std::is_floating_point<T>::value),
          "Желаю аргумент в виде целого или числа с плавающей!"
        );
        _value = value;
      }

    То сборка происходит с ошибкой:

    ExpandedWrap disabled
      ld.lld: error: undefined symbol: AnyDigit::AnyDigit<double>(double)
      >>> referenced by ..\../main.cpp:7
      >>>               debug/main.o:(_main)
      clang++: error: linker command failed with exit code 1 (use -v to see invocation)
      mingw32-make[1]: *** [Makefile.Debug:71: debug/AnyDigit.exe] Error 1

    Ну и, собственно, вопрос: что я делаю не так, и как это сделать правильно? Я имею ввиду разнесение кода на объявление и реализацию в разные файлы при реализации шаблонного конструктора (возможно и методов - в будущем).
      Определение шаблона должно быть видно компилятору в момент инстанцирования. Поскольку вы унесли его в другой .cpp - компилятор его не видит и понятия не имеет, как именно ему инстанцировать шаблон. Оставьте реализацию в заголовочном файле. Если сомневаетесь - посмотрите любой системный заголовочный файл.

      Добавлено: в .cpp можно (нужно) уносить полную специализацию шаблона. Т.е. если для AnyDigit<uint8_t> у вас будет какая-то особая реализация конструктора - в заголовочном вы пишете
      ExpandedWrap disabled
        template<> AnyDigit<uint8_t>(const uint8_t value);
      а в .cpp уносите
      ExpandedWrap disabled
        template<> AnyDigit<uint8_t>(const uint8_t value)
        : _value(value)
        {
            std::cout<<"Слава мне, победителю драконов!";
        }
      Сообщение отредактировано: Dushevny -
        Цитата Dushevny @
        Определение шаблона должно быть видно компилятору в момент инстанцирования. Поскольку вы унесли его в другой .cpp - компилятор его не видит и понятия не имеет, как именно ему инстанцировать шаблон. Оставьте реализацию в заголовочном файле. Если сомневаетесь - посмотрите любой системный заголовочный файл.

        Спасибо! Да нет, уже не сомневаюсь. Догадывался об этом, но думал, что я где-то в чём-то ошибаюсь, упускаю и есть более другие решения.
          Цитата Dushevny @
          Определение шаблона должно быть видно компилятору в момент инстанцирования. Поскольку вы унесли его в другой .cpp - компилятор его не видит и понятия не имеет, как именно ему инстанцировать шаблон.
          Не совсем так. Компилятор не конкретизирует шаблон, если не будет видеть его определения, но это не значит, что программа неверна. Как и в случае обычных функций, компилятору достаточно объявления, чтобы проверить корректность использования, при необходимости скастить параметры, сгенерировать код вызова и понять, что и как делать с результатом.
          Проблема в том, что компилятор – как и в случае обычных функций – полагает, что где-то это определение есть, и если оно есть, то ок. Только вот с шаблонами часто получается так, что его нет. Когда где-то там, компилятор встретит определение шаблона, он понятия не имеет, где и с какими наборами параметров он был использован. Получается, что в точке использования нет определения, поэтому компилер не в состоянии сгенерирвоать конкретизацию и вставляет просто вызов инстанцированного шаблона, а в точке, где доступно определение, обычно нет использования с теми же наборами параметров, поэтому компилер этой конкретизации не сгенерировал. В итоге линкер ругается на отсутствующий символ.
          Проблема решатся тремя путями. Самый правильный – экспорт шаблонов. Увы, но ленивые разработчики реализаций по всему миру саботировали эту фичу, и единственные, кто её в своё время реализовал ≈20 лет назад – это EDS, известная своими front-end решениями для других. Например, Intel C++ Compiler юзает именно их. Правда, экспорт шаблонов они лицензировали у них только под линух. Из-за ленности разработчиков Комитет в C++11 объявил экспорт шаблонов опциональной фичей. Второй по популярности – явные инстанцирования. Там, где доступно определение, можно нафигачить явных запросов на инстанцирование, с теми же наборами параметров, с каковыми шаблон используется где-то там. В итоге компилятор нагенерит инстанцирований без неявных запросов, и у линкера всё срастётся. Ну и третий самый дешёвый для нас, но не компилятора, каковой практически повсеместно и используется – предоставлять компилятору определение всегда, чтобы он мог генерировать конкретизации везде, где они неявно запрашиваются, а проблемы с дубликатами, каковых в итоге по всей программе может оказаться неимоверное количество, становятся проблемами компилятора и линкера, которые они обязаны решить самостоятельно.
          Сообщение отредактировано: Qraizer -
            Ничего не понял, но очень интересно! :lol: Как-то в юности один из моих преподов прямо вбил мне в мозг фразу "Не учи меня жить - помоги материально!". Конечно всё в переносном смысле, а если в прямом "Хватит объяснений - скажи чего делать-та!". Qraizer, собственно и у меня вопрос - реализовывать тела шаблонных конструкторов и методов прямо в заголовочном файле - это единственный выход, или где? :-?

            Добавлено
            Цитата Dushevny @
            Поскольку вы унесли его в другой .cpp - компилятор его не видит и понятия не имеет, как именно ему инстанцировать шаблон.

            И, кстати да, утверждение не совсем верное - не компилятор ругается, а линкер. К тому времени компилятор уже давно и умело покинул оперативную память.

            Добавлено
            Цитата Dushevny @
            Определение шаблона должно быть видно компилятору в момент инстанцирования. Поскольку вы унесли его в другой .cpp - компилятор его не видит и понятия не имеет, как именно ему инстанцировать шаблон. Оставьте реализацию в заголовочном файле. Если сомневаетесь - посмотрите любой системный заголовочный файл.

            Добавлено: в .cpp можно (нужно) уносить полную специализацию шаблона. Т.е. если для AnyDigit<uint8_t> у вас будет какая-то особая реализация конструктора - в заголовочном вы пишете

            Вот тут и засада. Я и без шаблонов могу нагенерить 100500 конструкторов с помощью нехитрого скрипта на ... Perl'овке! Как два байта об асфальт :lool: Но смысл тогда в метапрограммировании (конкретно в моем случае)?!!
              Цитата Majestio @
              Ничего не понял, но очень интересно!
              Чуть подробнее. Например:
              ExpandedWrap disabled
                // q.h
                template <typename T>
                T add(const T& a, const T& b);
              ExpandedWrap disabled
                // q1.cpp
                #include "q.h"
                 
                template <typename T>
                T add(const T& a, const T& b)
                {
                  return a + b;
                }
              ExpandedWrap disabled
                // q2.cpp
                #include <iostream>
                #include "q.h"
                 
                int main()
                {
                  std::cout << add(12, 34) << std::endl;
                }
              Когда компилятор в q2.cpp видит add(12, 34), он вполне способен разрулить объявление в q.h, чекнуть использование и вызвать, но не сгенерировать конкретизацию для T == int, т.к. не видит определения add<>(). Когда компилятор видит это определение в q1.cpp, он вполне способен сгенерировать конкретизацию для T == int, но не в курсе, что должен это сделать, т.к. не видит никакого использования add<>(). В итоге в q2 будет внешняя ссылка, а в q1 не будет конкретизации:
              ExpandedWrap disabled
                q2.obj : error LNK2019: ссылка на неразрешенный внешний символ "int __cdecl add<int>(int const &,int const &)" (??$add@H@@YAHABH0@Z) в функции _main.
                q1.exe : fatal error LNK1120: неразрешенных внешних элементов: 1

              Решаем через экспорт шаблонов:
              ExpandedWrap disabled
                // q.h
                export template <typename T>
                T add(const T& a, const T& b);
              Это должно бы решить проблему, если бы не но:
              ExpandedWrap disabled
                D:\Work\delMe\q.h(2): error C3348: экспортированные шаблоны не являются частью текущих стандартов C++
              А всё потому, что:
              Цитата Working Draft, Standard for Programming Language C++
              C.2.7 Clause 17: templates [diff.cpp03.temp]
              1 Affected subclause: 17.1
              Change: Remove export.
              Rationale: No implementation consensus.
              Effect on original feature: A valid C++ 2003 declaration containing export is ill-formed in this International Standard.

              Ок, решаем через явную конкретизацию:
              ExpandedWrap disabled
                // q1.cpp
                #include "q.h"
                 
                template <typename T>
                T add(const T& a, const T& b)
                {
                  return a + b;
                }
                 
                template
                int add<int>(const int&, const int&);
              Явный запрос на генерацию конкретизации для T == int вынуждает компилер это сделать даже притом, что неявных запросов не было. Теперь линкер будет доволен, и оно даже будет работать:
              ExpandedWrap disabled
                /out:q1.exe
                q1.obj
                q2.obj
                 
                D:\Work\delMe>q1
                46
              Когда-то так пробовали делать: в спецом предусмотренной для этого единице трансляции хреначили все явные запросы. Только вот задолбались в итоге это делать. К тому же это не полностью решает проблему, т.к. в точке использования контекст может отличаться от контекста в точке инстанцирования. (В общем-то именно это и послужило причиной саботажа экспорта шаблонов. Очень тяжело переносить контексты видимых имён из одной единицы трансляции в другую. Притом, что всякие make тоже задолбаются учитывать зависимости. EDS так прямо и заявила, что мол они-то молодцы, реализовали, только вот заняло это у них полтора человеко-года. К тому же это единственная фича C++, которая требует от компилятора способности работать сразу с несколькими контекстами имён, причём ещё и переключать их на лету.) К примеру чуть более сложная
              ExpandedWrap disabled
                template <typename T>
                T add(const T& a, const T& b)
                {
                  return f(a) + f(b);
                }
              начинает быть зависимой от некой там f(), которая тоже не пойми что, и в разных единицах трансляции может оказаться совсем разными сущностями.

              Резюме. Третий вариант с реализацией сразу в q.h и без всяких q1.cpp нынче рулит.
              В отношении твоего вопроса о метакодинге, получается, что второй вариант тебе просто бесполезен, т.к. ты и так видишь, какие конкретизации явно запрашиваешь. Ну и т.к. первый вариант недоступен, то да, остаётся исключительно третий.
              Сообщение отредактировано: Qraizer -
                Цитата Qraizer @
                Резюме. Третий вариант с реализацией сразу в q.h и без всяких q1.cpp нынче рулит.

                Н-да ... делать нечего, надо принимать "правила игры" :-?
                0 пользователей читают эту тему (0 гостей и 0 скрытых пользователей)
                0 пользователей:


                Рейтинг@Mail.ru
                [ Script execution time: 0,0447 ]   [ 16 queries used ]   [ Generated: 8.09.24, 07:09 GMT ]