1

Lets say we have a templated class template< typename T> FOO that wraps a T pointer and we want to have a user defined conversion for getting a FOO<T const> & that points to an instantiation of FOO. The following reproducer code illustrates the problems that appear to arise (Note that the pointer conversions are there for comparison)

#include<iostream>

template< typename T>
class FOO
{
public:
  template< typename U=T >
  operator typename std::enable_if< !std::is_const<U>::value, FOO<T const> & >::type ()
  {
    std::cout<<"calling FOO<T>::operator FOO<T const>&()"<<std::endl;
    return reinterpret_cast< FOO<T const> &>(*this);
  }

  template< typename U=T >
  operator typename std::enable_if< !std::is_const<U>::value, FOO<T const> * >::type ()
  {
    std::cout<<"calling FOO<T>::operator FOO<T const>*()"<<std::endl;
    return reinterpret_cast< FOO<T const> *>(this);
  }

  T * m_data = nullptr;
};

int main()
{
  FOO<int> foo;

  FOO<int const> & fooToConst  = foo;                                // conversion 1r
  FOO<int const> & fooToConst2  = static_cast<FOO<int const>&>(foo); // conversion 2r
  FOO<int const> & fooToConst3 = foo.operator FOO<int const>&();     // conversion 3r

  FOO<int const> * pfooToConst  = foo;                               // conversion 1p
  FOO<int const> * pfooToConst2 = static_cast<FOO<int const>*>(foo); // conversion 2p
  FOO<int const> * pfooToConst3 = foo.operator FOO<int const>*();    // conversion 3p
  return 0;
}

compilation with gcc8.1.0 g++ -std=c++14 main.cpp everything works and the output looks like:

calling FOO<T>::operator FOO<T const>&()
calling FOO<T>::operator FOO<T const>&()
calling FOO<T>::operator FOO<T const>&()
calling FOO<T>::operator FOO<T const>*()
calling FOO<T>::operator FOO<T const>*()
calling FOO<T>::operator FOO<T const>*()

Compilation with clang6.0 clang++ -std=c++14 main.cpp fails with:

main.cpp:29:20: error: non-const lvalue reference to type 'FOO<const int>' cannot bind to a value of unrelated type 'FOO<int>'
  FOO<int const> & fooToConst  = foo;                                // conversion 1r
                   ^             ~~~
main.cpp:30:35: error: non-const lvalue reference to type 'FOO<const int>' cannot bind to a value of unrelated type 'FOO<int>'
  FOO<int const> & fooToConst2  = static_cast<FOO<int const>&>(foo); // conversion 2r
                                  ^                            ~~~

All other conversion (3r,1p,2p,3p) work for clang.

So the question is...Is gcc correct, or is clang correct?

  • If clang is correct, what is the reason that conversions (1r,2r) code shouldn't work?

    • I know the pointer conversions are a little wacky, but why is (1p, 2p) accepted, while (1r, 2r) are not?
    • And why does gcc allow them?
  • If gcc is correct, is this a bug in clang?

EDIT If conversions attempts for (1r,2r) are changed to:

FOO<int const> const & fooToConst  = foo;                                      // conversion 1r
FOO<int const> const & fooToConst2  = static_cast<FOO<int const> const&>(foo); // conversion 2r

it all works on clang! Why should that be?

doc07b5
  • 600
  • 1
  • 7
  • 18
  • 2
    A `Foo` is a completely different type from a `Foo`. It is UB to cast one to the other – NathanOliver Dec 03 '18 at 18:26
  • Note that in your code `U` is always `T`, so `!std::is_const::value` check and entire `std::enable_if` are useless – user7860670 Dec 03 '18 at 18:27
  • @NathanOliver. Yes, that would be the answer to "can i cast from Foo to Foo safely"....which is a different question...or is it why this doesn't work? – doc07b5 Dec 03 '18 at 18:36
  • @VTT If ```T``` is ```const int```, then the UDC wouldn't be defined in the class ```FOO```. So the ```std::enable_if< !std::is_const::value >``` is intended to serve that purpose. Is this not what would result from that check? – doc07b5 Dec 03 '18 at 18:39
  • Have you read over the rules by which `template<>operator T` has its type deduced? In the standard? That stuff is quirky enough that "a compiler compiles it" isn't going to be strong evidence that you did it right, especially if you mix SFINAE. – Yakk - Adam Nevraumont Dec 03 '18 at 18:48
  • Why do you want to prohibit UDC for `const int` `T`? I guess for test purposes it is fine. – user7860670 Dec 03 '18 at 19:23

1 Answers1

1

This "answer" addresses the practical problem rather than which (clang or gcc) is right. I include it as it might be helpful to the OP, even if it isn't good enough for a answer at this point.

template<class X>
struct get_template_arg0;
template<class X>
using get_template_arg0_t=typename get_template_arg0<X>::type;

template<template<class...>class Z, class T>
struct get_template_arg0<Z<T>> {
    using type=T;
};

template< typename T>
class FOO
{
public:
  template< typename U, 
    std::enable_if_t<
        std::is_same<std::remove_const_t<get_template_arg0_t<U>>, T>{}
        && !std::is_same<get_template_arg0_t<U>, T>{},
        bool
    > =true
  >
  operator U& ()
  {
    std::cout<<"calling FOO<T>::operator FOO<T const>&()"<<std::endl;
    return reinterpret_cast< FOO<T const> &>(*this);
  }

  template< typename U, 
    std::enable_if_t<
        std::is_same<std::remove_const_t<get_template_arg0_t<U>>, T>{}
        && !std::is_same<get_template_arg0_t<U>, T>{},
        bool
    > =true
  >
  operator U*()
  {
    std::cout<<"calling FOO<T>::operator FOO<T const>*()"<<std::endl;
    return reinterpret_cast< FOO<T const> *>(this);
  }

  T * m_data = nullptr;
};

I simplify the deduction of the type in the template conversion operators, then add SFINAE tests.

Both clang++ and g++ compile this "correctly".

(As others have noted, your reintepret_casts make the program exhibit undefined behavior.)

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524