Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Prevent implicit conversion of constructor arguments to external library type

Consider the following code:

#include <boost/range.hpp>
#include <boost/iterator/counting_iterator.hpp>

typedef boost::iterator_range<boost::counting_iterator<int>> int_range;

template <typename T>
class Ref {
    T* p_;    
  public:    
    Ref(T* p) : p_(p) { }
    /* possibly other implicit conversion constructors,
       but no unconstrained template constructors that don't
       use the explicit keyword... */  
    operator T*() const { return p_; }
    operator const T*() const { return p_; }    
};

struct Bar { };

class Foo {    
  public:    
    Foo(int a, char b) { /* ... */ }    
    Foo(int a, const Ref<Bar>& b) { /* ... */ }     
    Foo(int a, const int_range& r) { /* ... */ }     
};

int main() {
  Bar b;
  Foo f(5, &b);
  return 0;
}

This code doesn't compile because the use of the Foo constructor is ambiguous, since boost::iterator_range apparently has a templated constructor that takes a single argument and is not declared as explicit. Assuming that changing the structure of Ref is not an option, how can I fix this problem? I came up with the following possible solution, but it's ugly and not easily maintainable, especially if there are more than a few constructors of Foo:

template<typename range_like>
Foo(
  int a, 
  const range_like& r,
  typename std::enable_if<
    not std::is_convertible<range_like, Ref<Bar>>::value
      and std::is_convertible<range_like, int_range>::value,
    bool
  >::type unused = false
) { /* ... */ } 

or similarly

template<typename range_like>
Foo(
  int a, 
  const range_like& r,
  typename std::enable_if<
    std::is_same<typename std::decay<range_like>::type, int_range>::value,
    bool
  >::type unused = false
) { /* ... */ } 

which has the disadvantage that all other implicit type conversion for int_range is disabled, and thus relies on unspecified features of boost (and my instincts tell me it's probably a bad idea anyway). Is there a better way to do this? (C++14 "concepts-lite" aside, which is really what this problem wants I think).

like image 274
Daisy Sophia Hollman Avatar asked Jan 15 '14 18:01

Daisy Sophia Hollman


1 Answers

I think this program is a minimal example of your problem:

#include <iostream>

struct T {};

struct A {
  A(T) {}
};

struct B {
  B(T) {}
};

struct C {
  C(A const&) { std::cout << "C(A)\n"; }
  C(B const&) { std::cout << "C(B)\n"; }
};

int main() {
  C c{T{}};
}

You have two types A and B both implicitly convertible from another type T, and another type C implicitly convertible from A and B, but for which implicit conversion from T is ambiguous. You desire to disambiguate the situation so that C is implicitly convertible from T using the sequence of conversions T => A => C, but you must do so without changing the definitions of A and B.

The obvious solution - already suggested in the comments - is to introduce a third converting constructor for C: C(T value) : C(A(value)) {}. You have rejected this solution as not general enough, but without clarifying what the "general" problem is.

I conjecture that the more general problem you want solved is to make C unambiguously implicitly convertible from any type U that is implicitly convertible to A using the sequence of conversions U => A => C. This is achievable by introducing an additional template constructor to C (Live code demo at Coliru):

template <typename U, typename=typename std::enable_if<
  !std::is_base_of<A,typename std::decay<U>::type>::value &&
   std::is_convertible<U&&, A>::value>::type>
C(U&& u) : C(A{std::forward<U>(u)}) {}

The template constructor is a direct match for C(U), and so is unambiguously preferred over the C(A) and C(B) constructors that would require a conversion. It is constrained to accept only types U such that

  • U is convertible to A (for obvious reasons)
  • U is not A or a reference to A or a type derived from A, to avoid ambiguity with the C(const A&) constructor and infinite recursion in the case that U is e.g. A& or A&&.

Notably this solution does not require changing the definitions of T, A, B, C(A const&) or C(B const&), so it is nicely self-contained.

like image 181
Casey Avatar answered Oct 21 '22 13:10

Casey