Forum Programmation.c++ Problème de templates: undefined reference

Posté par  .
Étiquettes : aucune
0
16
déc.
2012

Salut,

J'essaye de coder une fonction en C++ qui sépare un URI en deux: le protocole, et le reste.

mailto:quux@foo.bar  -> ("mailto", "quux@foo.bar")

La fonction doit donc retourner deux valeur. J'ai utilisé pour cela un conteneur std::pair<std::string, std::string>. Elle prends en argument un std::string.

namespace URI {
    std::pair<std::string,std::string> splitScheme (const std::string & str);
}

Jusque là, pas de problèmes: la fonction était sale mais marchais bien.

Là où ça se gâte, c'est que je ne suis pas sûr de toujours utiliser une std::string: je voudrais que ma fonction fonctionne aussi avec des std::wstring et des std::u16string.

J'ai donc essayé de transformer ma fonction en fonction template:

template <class STR>
std::pair<STR, STR> splitScheme (const STR & str);

Au final, j'ai le code suivant:

// URI.hpp
#ifndef _DEF_URI_HPP_
#define _DEF_URI_HPP_

#include <string>
#include <utility>
#include <iostream>

namespace URI {
    template <class STR> std::pair<STR, STR> splitScheme (const STR & str);
}

#endif

 

// URI.cpp
#include "URI.hpp"

template <class STR>
std::pair<STR, STR> URI::splitScheme (const STR & str) {
    typename STR::size_type s = str.find(':');
    typename STR::value_type c;
    unsigned int i;

    // <code sale>
    if ((s == STR::npos) || str.empty())
        return make_pair("", str);
    else {
        for (i = 0; i < s; i++) {
            c = str.at(i);
            if (!((c >= 'A' && c <= 'Z') 
                    || (c >= 'a' && c <= 'z')
                    || ((i >= 1) && (c >= '0' && c <= '9'))
                    || ((i >= 1) && (c == '+' || c == '-' || c == '.' ))))
                return make_pair("", str);
        }
        return make_pair(str.substr(0, i), str.substr(s+1));

    }
    // </code sale>
}

 

#include "URI.hpp"

#include <string>
#include <iostream>
#include <utility>

int main (int argc, char ** argv)  {
    if (argc != 2) {
        std::cerr << "Usage: " << argv[0] << " uri" << std::endl;
        return 1;
    }

    std::string s(argv[1]);
    std::pair<std::string,std::string> p;

    p = URI::splitScheme<std::string>(s);

    if (!p.first.empty())
        std::cout << "Scheme: " << p.first << std::endl;

    std::cout << "2nd part: " << p.second << std::endl;

    return 0;
}

Au final, quand je compile le code, j'obtiens un message d'erreur de ld:

% g++ URI.cpp main.cpp -o test
/tmp/cc1YnskK.o: In function `main':
main.cpp:(.text+0xb5): undefined reference to `std::pair<std::string, std::string> URI::splitScheme<std::string>(std::string const&)'
collect2: error: ld returned 1 exit status

Si je déclare manuellement le prototype non défini, ça ne fait rien de plus:

template <> std::pair<std::string, std::string> splitScheme (const std::string & str);

Quelqu'un a une idée ? Peut être qu'utiliser un std::pair n'est pas la bonne
méthode ?

Merci d'avance :-)

  • # Templates et C++03

    Posté par  . Évalué à 3.

    N'oublie pas que les templates génèrent du code pour chaque paramètre template différent. Cela veut dire que si le compilateur n'a pas le code de URI::splitScheme lorsque tu l'utilise, alors il ne pourra pas le générer tout seul. Il faut voir ça un peu comme une macro: il faut qu'elle soit définie pour être utilisable.

    en C++03 en tout cas. Normalement, cela n'est plus nécessaire en C++11 (le standard oblige à ce que ça marche dans ton cas), mais gcc ne le supporte pas (ou du moins, pas à ma connaissance).

    En C++03, tu à le choix entre :
    - Mettre le code de splitScheme dans un header et l'inclure.
    - déclarer explicitement quel intanciations tu à besoin dans URI.cpp, et utiliser export, sachant que très peu de compilos le supportent (et ceux qui le supportent pensent que c'est une mauvaise idée) à tel point que ça à été enlevé dans C++11.

    Ouais, donc en gros, tu n'a pas le choix quoi.

    • [^] # Re: Templates et C++03

      Posté par  . Évalué à -1.

      Ok, merci :-)

      Effectivement, en mettant tout le code dans le header, ça marche… mais ce n'est pas très propre…

      En fait, puisque les classes std::u16string et std::string dérivent de std::basic_string, j'imagine que je peux utiliser cette classe pour ma fonction, et faire du type casting si j'ai besoin de std::u16string

      Envoyé depuis ma Debian avec Firefox

      • [^] # Re: Templates et C++03

        Posté par  . Évalué à 1.

        Effectivement, en mettant tout le code dans le header, ça marche… mais ce n'est pas très propre…

        J'ai du mal à voir en quoi mettre des macro dans les headers est "propre" alors que mettre un métaprogramme template dans les headers n'est "pas très propre". Les deux sont de la métaprogrammation. Ce n'est pas parce que l'un ressemble à une implémentation que ça en est une.

        Sinon, std::string n'hérite pas de std::basic_string. std::string est exactement std::basic_string<char>. Il n'y a pas de lien d'héritage entre un patron et son instanciation. En fait, il n'y a pas de lien du tout. On peut très bien avoir un patron:

        template <typename A>
        class B {
            void coucou();
        };
        
        

        et décider que B<int> est totalement différent…

        template <>
        class B<int> : std::runtime_error {
        private:
            void cocu();
        public:
            B(float i);
            const char* what() const; // noexcept en C++11
        protected:
            double quarante_deux;
        };
        
        

        (ce n'est pas le cas de std::basic_string<char>sur les bonnes implémentations, mais ça pourrai).

        Pour cette raison, si tu veut à la fois supporter les std::string et std::u32string, tu n'a pas vraiment le choix que d'utiliser un template. (bon, ok, tu peux utiliser boost::variant ou boost::any, mais c'est franchement se tirer une balle dans le pied).

  • # Include à l'envers :-)

    Posté par  . Évalué à 2. Dernière modification le 17 décembre 2012 à 10:32.

    Dans les template, tout est un header : le fichier .cpp n'est pas compilé en un fichier objet vu qu'il est générique.

    Il faut donc inclure le cpp à la fin du hpp, et non pas le hpp au début du cpp. (et éventuellement renommer le cpp en tpp pour préciser qu'il s'agit d'un template ?)

    (après c'est peut-être une méthode très sale, si quelqu'un a mieux je suis preneur).

    • [^] # Re: Include à l'envers :-)

      Posté par  (site web personnel) . Évalué à 1.

      et éventuellement renommer le cpp en tpp pour préciser qu'il s'agit d'un template ?)

      find /usr/include/c++/4.7 (et une paire d'yeux aguerris) nous informe que gcc stocke ça sous forme de .tcc.

  • # 3 solutions

    Posté par  (site web personnel) . Évalué à 1.

    Comme dit plus haut le compilateur doit être capable d'instancier les template pour les type particuler.
    Je propose 3 solutions:

    1- Comme déjà proposé, mettre l'implémentation dans le fichier entête (URI.hpp)

    2- Si tu sais à l'avance quels type seront utilisé, tu peux faire une instentiation explicite dans URI.cpp

    template std::pair<std::string, std::string> URI::splitScheme (const std::string & str);
    template std::pair<std::wstring, std::wstring> URI::splitScheme (const std::wstring & str);
    
    

    Cela dit au compilateur de générer le code pour ton template pour les type donnés lors de la compilation de URI.cpp, et permettra au linkeur de les trouver lors de l'édition des liens.

    3- export template: dans ton header rajoute le mot clef export

    export template<class STR> std::pair<STR, STR> splitScheme (const STR & str);
    
    

    Mais oublie cette dernière technique, car quasiment aucun compilateur ne supporte ce mot clef. Et il a d'ailleurs été retiré en C++11:
    http://en.wikipedia.org/wiki/C++#Exported_templates

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.