std::enable_if sur un constructeur ignoré par mon compilateur

Le problème exposé dans ce sujet a été résolu.

Bonjour !

Je bute sur un problème sur ma première utilisation de std::enable_if, quelques recherches ne m'ont pas permis de comprendre le problème, je me tourne donc vers vous.

J'ai une classe template ObjectRef, qui est assez similaire à std::shared_ptr excepté que le compteur de référence est contenu dans l'objet lui-même (qui doit alors dériver d'une autre classe).

J'ai de nombreux types dans le projet qui utilisent (et spécialisent ce template), notamment deux ici qui posent problème:

1
2
3
using NzUberShaderRef = NzObjectRef<NzUberShader>;

using NzUberShaderPreprocessorRef = NzObjectRef<NzUberShaderPreprocessor>;

En sachant que NzUberShaderPreprocessor hérite directement et publiquement de NzUberShader, le problème vient lorsque je veux passer un NzUberShaderPreprocessorRef à une fonction attendant un NzUberShaderRef.

Afin de régler le problème, j'ai décidé de rajouter un constructeur à ma classe ObjectRef, prenant une référence constante vers un ObjectRef d'un autre type si et seulement si cet autre type est implicitement convertible vers le premier type (à l'aide de std::enable_if).
Ce qui nous donne ceci en termes de code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
template<typename T>
class NzObjectRef
{
    static_assert(std::is_base_of<NzRefCounted, T>::value, "ObjectRef shall only be used with RefCounted-derived type");

    public:
        NzObjectRef();
        NzObjectRef(T* object);
        template<typename U> NzObjectRef(typename std::enable_if<std::is_convertible<U*, T*>::value, const NzObjectRef<U>&>::type ref);

...

        NzObjectRef& operator=(T* object);
        template<typename U> NzObjectRef& operator=(typename std::enable_if<std::is_convertible<U*, T*>::value, const NzObjectRef<U>&>::type ref);

Mais voilà, le problème c'est que j'ai l'impression que le compilateur ignore royalement ce nouveau constructeur, la compilation fonctionne jusqu'à ce que j'ai cette erreur:

no known conversion for argument 2 from 'NzUberShaderPreprocessorRef {aka NzObjectRef<NzUberShaderPreprocessor>}' to 'NzObjectRef<NzUberShader>'|

Code minimal reproduisant le problème:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void test(NzObjectRef<NzUberShader> ref)
{
    std::cout << "Oh hai" << std::endl;
}

int main()
{
    std::cout << std::is_convertible<NzUberShaderPreprocessor*, NzUberShader*>() << std::endl; // Affiche bien 1

    NzObjectRef<NzUberShaderPreprocessor> ref;

    test(ref); // Erreur ici

    return 0;
}

Alors évidemment si je vire le std::enable_if pour le remplacer directement par le type template, tout fonctionne.

1
        template<typename U> NzObjectRef(const NzObjectRef<U>& ref);

Donc je ne suis pas bloqué, mais j'aimerai bien comprendre quel est le problème, std::enable_if étant censé être remplacé par le type si la condition est validée (et elle est validée).

Donc voilà, si quelqu'un avait l'explication (et/ou le correctif), je lui en serais reconnaissant :)

+0 -0

Non je n'avais pas testé, et je dois dire que ça vient d'ajouter à mon incompréhension car:

1
NzObjectRef<NzUberShader> ref ( NzObjectRef<NzUberShaderPreprocessor>() );

compile parfaitement, mais:

1
2
NzObjectRef<NzUberShaderPreprocessor> uberRef;
NzObjectRef<NzUberShader> ref(uberRef);

ne compile pas, avec la même erreur, que voici (je poste le log complet):

E:\NazaraEngine\NazaraTest\main.cpp|20|error: no matching function for call to 'NzObjectRef<NzUberShader>::NzObjectRef(NzObjectRef<NzUberShaderPreprocessor>&)'|
E:\NazaraEngine\NazaraTest\main.cpp|20|note: candidates are:|
..\include\Nazara\Core\ObjectRef.inl|38|note: NzObjectRef<T>::NzObjectRef(NzObjectRef<T>&&) [with T = NzUberShader]|
..\include\Nazara\Core\ObjectRef.inl|38|note: no known conversion for argument 1 from 'NzObjectRef<NzUberShaderPreprocessor>' to 'NzObjectRef<NzUberShader>&&'|
..\include\Nazara\Core\ObjectRef.inl|30|note: NzObjectRef<T>::NzObjectRef(const NzObjectRef<T>&) [with T = NzUberShader]|
..\include\Nazara\Core\ObjectRef.inl|30|note: no known conversion for argument 1 from 'NzObjectRef<NzUberShaderPreprocessor>' to 'const NzObjectRef<NzUberShader>&'| ..\include\Nazara\Core\ObjectRef.hpp|22|note: template<class U> NzObjectRef<T>::NzObjectRef(const typename std::enable_if<std::is_convertible<U, T>::value, NzObjectRef<U> >::type&)|
..\include\Nazara\Core\ObjectRef.hpp|22|note: template argument deduction/substitution failed:|
E:\NazaraEngine\NazaraTest\main.cpp|20|note: couldn't deduce template parameter 'U'|
..\include\Nazara\Core\ObjectRef.inl|15|note: NzObjectRef<T>::NzObjectRef(T) [with T = NzUberShader]|
..\include\Nazara\Core\ObjectRef.inl|15|note: no known conversion for argument 1 from 'NzObjectRef<NzUberShaderPreprocessor>' to 'NzUberShader
'| ..\include\Nazara\Core\ObjectRef.inl|9|note: NzObjectRef<T>::NzObjectRef() [with T = NzUberShader]|
..\include\Nazara\Core\ObjectRef.inl|9|note: candidate expects 0 arguments, 1 provided|

+0 -0

Ça doit venir du fait que tu utilises U avant sa définition. (ne serait-ce que parce que la version sans enable_if fonctionne et si tu fais ceci, ça fonctionne également :

1
template<typename U> NzObjectRef(const NzObjectRef<U> & ref2, typename std::enable_if<std::is_convertible<U*, T*>::value, const NzObjectRef<U>&>::type ref) {}
+1 -0

Je ne suis pas certain d'avoir compris en quoi je l'utilisais avant sa définition, et encore moins d'avoir compris comment régler ça (tout en ne gardant qu'un argument), mais au moins tu sembles avoir compris le problème :D

+0 -0

@minirop : je vois la logique qu'il y a derrière, il y a une limite à ce que peut faire l'inférence de type au niveau du template. On lui demande de matcher avec enable_if<constexpr>::type, le problème étant que tant qu'il n'a pas calculé si la constexpr, il ne sait même pas si type existe, et la constexpr dépend elle même de type. Mais ça n'explique pas du tout pourquoi le compilateur est suffisamment malin pour le faire quand on lui file un appel de constructeur en paramètre qui fondamentalement nous fournit un objet du même type.

EDIT : pendant les tests : ah putain le fumier j'ai compris. comme c'est un foutu constructeur sans paramètre qui ne fait rien qu'on lui passe, il le supprime purement et simplement, donc pas de type à déduire !

Fonctionne :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
template<class T>
class Bidule{
public:
  Bidule(){}
  Bidule(Bidule const&){}

  template<class U> 
  Bidule(typename std::enable_if<std::is_convertible<U,T>::value, Bidule<U>>::type const& o){}
};

int main(){
  Bidule<char> b( Bidule<int>() );
}

Echoue :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
template<class T>
class Bidule{
public:
  Bidule(int i){}
  Bidule(Bidule const&){}

  template<class U> 
  Bidule(typename std::enable_if<std::is_convertible<U,T>::value, Bidule<U>>::type const& o){}
};

int main(){
  Bidule<char> b( Bidule<int>(42) );
}

Pour avoir un truc qui marche, tu pourrais faire ça :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template<class T>
class Bidule{
public:
  Bidule() = default;

  template<class U> 
  Bidule(Bidule<U>const& o){
    static_assert(std::is_convertible<U,T>::value, "Message");
  }
};

Mais encore une fois, avec ce merdier du constructeur vide, le compilateur ne t'enverra bouler que sur les appels qui reçoivent un objet déjà construit O_o :

1
2
3
4
5
6
7
8
class A{};

int main(){
  Bidule<char> a(Bidule<A>()); //accepté

  Bidule<A> b;
  Bidule<char> c( b );         //rejeté
}

C'est encore mieux que rien, je vais rajouter un static_assert comme tu me le conseilles, juste histoire d'avoir un message d'erreur un peu plus digeste.

Merci à vous deux en tout cas de vous êtres penchés sur le problème, c'est pas encore tout de suite que j'utiliserais std::enable_if on dirait.

+0 -0

On est des gros neuneus vous savez.

Cours de C++, que fais ceci :

1
Bidule<A> o();

Réponse : ça nous dit qu'il existe, une fonction "o", renvoyant "Bidule" et ne prenant pas de paramètre. Donc, que fait ceci :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <type_traits>

template<class T>
class Bidule{
public:
  Bidule();

  template<class U> 
  Bidule(Bidule<U>const& o){
    static_assert(std::is_convertible<U,T>::value, "Message");
  }

  void show(){}
};

class A{};

int main(){
  Bidule<char> a(Bidule<A>());
  a.show();
}

Réponse :

1
2
3
4
test_implicit_template.cpp: In function ‘int main()’:
test_implicit_template.cpp:34:5: error: request for member ‘show’ in ‘a’, which is of non-class type ‘Bidule<char>(Bidule<A> (*)())’
   a.show();
     ^

Grumblblblblblblbl !!

Et bien entendu, tout est OK en fait avec le static_assert :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <type_traits>

template<class T>
class Bidule{
public:
  Bidule();

  template<class U> 
  Bidule(Bidule<U>const& o){
    static_assert(std::is_convertible<U,T>::value, "Message");
  }
};

class A{};

int main(){
  Bidule<char> a(Bidule<A>{});
}
1
2
3
4
5
test_implicit_template.cpp: In instantiation of ‘Bidule<T>::Bidule(const Bidule<U>&) [with U = A; T = char]’:
test_implicit_template.cpp:18:29:   required from here
test_implicit_template.cpp:11:5: error: static assertion failed: Message
     static_assert(std::is_convertible<U,T>::value, "Message");
     ^
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte