Implicit Conversion Operators

closed account (zb0S216C)
People claim that implicit conversion operators are bad practice, since they allow seemingly unexpected conversions. We all know an object is implicitly converted to a bool in a Boolean context. So, what about an implicit bool conversion operator? Are they bad?

1
2
3
4
5
6
class Lemon_Fudge_Cake
{
    public: Lemon_Fudge_Cake() : Pointer_To_Lemon__(0x0) { }
    private: int *Pointer_To_Lemon__;
    public: operator bool () const { return(this->Pointer_To_Lemon__); }
};

Thanks in advance.

Wazzak
Last edited on
We all know an object is implicitly converted to a bool in a Boolean context.

Are you sure? Or are you thinking about pointers?

For me, this fails to compile.

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

class Lemon_Fudge_Cake
{
	public: Lemon_Fudge_Cake() : Pointer_To_Lemon__(0x0) { }
	private: int *Pointer_To_Lemon__;
	// no operator bool()
};

int main()
{
	Lemon_Fudge_Cake cake;

	if(cake)
		cout << "cake" << endl;
	else
		cout << "not cake" << endl;

	return 0;
}


1>------ Build started: Project: temp_test, Configuration: Release Win32 ------
1>Compiling...
1>temp_test.cpp
1>.\temp_test.cpp(15) : error C2451: conditional expression of type 'Lemon_Fudge_Cake' is illegal
1>        No user-defined-conversion operator available that can perform this conversion, or the operator cannot be called
1>Build log was saved at "file://w:\Source\Explore\cplusplus_vc9\temp_test\Release\BuildLog.htm"
1>temp_test - 1 error(s), 0 warning(s)
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========


Setting aside the dangers of accidental conversion, I have no idea what such a cast (cake -> bool) would mean. Your definition of the cast looks like it means it has lemon in it. Which would be better expressed by a bool containLemon() method.
Last edited on
Are you sure? Or are you thinking about pointers?

He's probably talking about implicit conversion operators.

Explicit conversion operators were introduced to get rid of some unwanted conversions, e.g. you can't accidentally pass a Lemon_Fudge_Cake instance to a function that expects a double as the argument anymore. However, the common cases where conversion to bool is used as a validity check still work:

1
2
3
if (cake)...;
bool cakeIsValid(cake);
bool cakesAreValid=cake && cake2;  


So explicit conversion operators should be preferred in most cases.
Last edited on
> People claim that explicit conversion operators are bad practice, since they allow seemingly unexpected conversions.

Implicit conversion operators are bad practice, if they allow seemingly unexpected conversions.
closed account (zb0S216C)
andywestken wrote:
"Or are you thinking about pointers?"

I used a pointer so that the Lemon_Fudge_Cake::operator bool()'s test was clear: To test Lemon_Fudge_Cake::Pointer_To_Lemo__ for null.

Athar wrote:
"He's probably talking about implicit conversion operators."

I've not heard of explicit conversion operators, only implicit ones. I'll read up on them.

JLBorges wrote:
"Implicit conversion operators are bad practice, if they allow seemingly unexpected conversions."

Can you provide an example? Also, I found an article regarding implicit conversion operators. Here's a quote from the article that caught my eye:

Article wrote:
"The upshot is that while conversion operators are handy, they compromise type safety and disarm the compiler of its type-safety checks."


Thanks for the replies :)

References:
[1] http://www.informit.com/guides/content.aspx?g=cplusplus&seqNum=297


Wazzak
> Can you provide an example?

A most commonly encountered example, where an implicit conversion is a very good idea, would be the implicit conversion from a const char* to a std::string.
1
2
3
4
5
int main( int argc, char* argv[] )
{
     std::vector< std::string > args( argv, argv+argc ) ;  
     // ...
}


Can you imagine programming in a language where an implicit conversion from short to int is not supported?


> they compromise type safety and disarm the compiler of its type-safety checks."

An implicit conversion never 'compromises type safety'. The problems with implicit conversions are a. it can violate the principle of least surprise. b. It can lead to compile-time errors because of ambiguity. For example:
1
2
3
4
5
6
7
8
9
10
struct A { A(char**) ; } ;

void foo(A) ;
void foo(int) ;

int main( int argc, char* argv[] )
{
    foo(argv) ; // typo: should have been foo(argc)
    // compiles cleanly: calls foo( A(argv) )
}

1
2
3
4
5
6
7
void foo( long ) ;
void foo( double ) ;

int main()
{
    foo(1234) ; // *** error - ambiguous
}


closed account (zb0S216C)
JLBorges wrote:
"Can you imagine programming in a language where an implicit conversion from short to int is not supported?"

Fair point.

JLBorges wrote:
"An implicit conversion never 'compromises type safety'."

Then why does MinGW accept this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Lamborghini
{
    operator bool() { return(false); }
};

struct Pagani
{
    operator bool() { return(false); }
};

int main(i)
{
    if(Lamborghini() && Pagani()); // OK
}

If I remove both bool conversion operators, the compiler throws a tantrum about the if. Is that not compromising type-safety checks?

Wazzak
Last edited on
> If I remove both bool conversion operators, the compiler throws a tantrum about the if.
> Is that not compromising type-safety checks?

No, it is not. Compile-time type safety is compromised if and only if code that results in a type error (for instance, operations valid on an object of type B are performed on an incompatible object of type A) is silently emitted. Needless to say, unless the programmer had explicitly instructed the compiler to remove the safety net by using a compile-time cast.

Run-time type safety is compromised if and only if code that results in a type error is executed.

The implicit conversion of a pointer/reference to a derived class object to a pointer/reference to the base class object does not compromise type safety.

An attempt at implicitly converting a pointer/reference to a base class object to a pointer/reference to a derived class object would have compromise compile-time type safety, had the compiler kept quiet about it

This C code is type-unsafe:
1
2
3
4
5
6
7
8
9
10
// *** C code ****
void c_function( int* pi )
{
    if( pi != NULL )
    {
        double* pd = pi ; // compromises compile-time type safety
        *pd += 7.8 ; // compromises run-time type safety
        // ...
    }
}


This C++ code is not. If any one of these constructs compromise type safety at compile-time, then all of them do. Obviously, none of these constructs compromise compile-time type safety.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
struct Lamborghini
{
    operator bool() const { return false ; }
    bool to_bool() const { return false ; }

    explicit operator int() const { return 100 ; }
    int foo() const { return 100 ; }
};

struct Pagani
{
    operator bool() const { return false ; }
    bool foo() const { return false ; }

    explicit operator int() const { return 200 ; }
    int to_int() const { return 200 ; }
};

int main()
{
    if( Lamborghini() && Pagani() ) { /* ... */ }
    if( Lamborghini() && bool( Pagani() ) ) { /* ... */ }
    if( bool( Lamborghini() ) && bool( Pagani() ) ) { /* ... */ }
    if( Lamborghini().operator bool() && Pagani().operator bool() ) { /* ... */ }
    if( Lamborghini().to_bool() && bool( Pagani() ) ) { /* ... */ }
    if( bool( Lamborghini() ) && Pagani().foo() ) { /* ... */ }
    if( Lamborghini().to_bool() && Pagani().foo() ) { /* ... */ }

    if( int( Lamborghini() ) < int( Pagani() ) ) { /* ... */ }
    if( Lamborghini().foo() < Pagani().to_int() ) { /* ... */ }
    if( int( Lamborghini() ) < int( Pagani() ) ) { /* ... */ }
    if( Lamborghini().foo() < int( Pagani() ) ) { /* ... */ }
    if( int( Lamborghini() ) < Pagani().to_int() ) { /* ... */ }
}
Last edited on
closed account (zb0S216C)
OK :) This is all coming together, now. So let's move towards a practical example. Let's say I had a marker class, which is basically a pointer.

1
2
3
4
5
6
7
8
9
template <typename T>
class Marker
{
    public:
        operator T *() { return(this->Pos__); }

    private:
        T *Pos__;
};

C++ is strict when it comes to pointers and what they point to; in terms of types. So:

1
2
3
4
5
6
int main()
{
    int A(0);
    Marker<int> NewMarker = {&A};
    int *Pointer(NewMarker);
}

...should be OK. If I understand this correctly, Marker::operator T *() doesn't affect type-safety checks because it returns the original Marker::Pos__ pointer. However, if I reinterpret_cast'd Marker::Pos__ before returning it, I would compromise type-safety? For instance:

1
2
3
4
5
6
7
8
9
class Marker
{
    public:
        template <typename T>
        operator T *() { return(reinterpret_cast<T*>(this->Pos__)); } // Idiocy.

    private:
        SomeClass *Pos__;
};

Would this not compromise type-safety?

Wazzak
Last edited on
> Would this not compromise type-safety?

Yes, it is a construct that would compromise type-safety.

It is also a construct that is completely irrelevant to the topic under discussion - there is no implicit conversion involved, let alone an implicit conversion via a user defined operator.

closed account (zb0S216C)
So how does one compromise type-safety? I don't want to, but I do want to know how to avoid it.

Wazzak
In general, by altogether avoiding (or being extremely careful with) compile-time cast operations static_cast<>, const_cast<> and reinterpret_cast<> (A dynamic_cast<> is type-safe).

In reality, type-safety goes much deeper than what a particular programming language is able to support. For example, let us say we are writing a program for a library, and two of our classes are book and member. Each book has a unique int id, and each library member too has a unique int id. So we write:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
struct book
{
      static int lookup_id( const char* name /* ... */ ) ;
      book( int id /* ... */ ) ;
      // ...
      int id() const { return id_ ; }
      // ...
      private: int id_ ;
      // ..
};

struct member
{
      static int lookup_id( const char* name /* ... */ ) ;
      member( int id /* ... */ ) ;
      // ...
      int id() const { return id_ ; }
      // ...
      private: int id_ ;
      // ..
};  

void foo( book& bk, member& m )
{
     int book_id = m.id() ; // type error
     int member_id = bk.id() ; // type error
     std::cout << "book id is: " << book_id  << '\n' ; // ???

     int i = book_id * member_id  ; // conceptually type unsafe
     // multiplying the id of a book with the id of a member violates the abstraction of their 'type'

     int id = book::lookup_id( "member name" /* ... */ ) ; // definitely type-unsafe
     member new_member(  id, /* ... */ ) ; // definitely type-unsafe
     // do things with member
}



In contrast this is type-safe:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
template< typename T > class numeric_id
{
    friend T ; // C++11
    int id ;
    numeric_id( int n ) : id(n) {} // implicit conversion
    operator int () const { return id ; } // implicit conversion
    inline friend std::ostream& operator<< ( std::ostream& stm, numeric_id<T> id )
    { return stm << id ; }

    public:
        numeric_id( const numeric_id<T>& ) = default ;
        numeric_id<T>& operator= ( const numeric_id<T>& ) = default ;
};

struct book
{
      static numeric_id<book> lookup_id( const char* title /* ... */ ) ;
      book( numeric_id<book> id /* ... */ ) : id_(id) { /* ... */ }
      // ...
      numeric_id<book> id() const { return id_ ; }
      // ...
      private: numeric_id<book> id_ ;
      // ..
};

struct member
{
      static numeric_id<member> lookup_id( const char* name /* ... */ ) ;
      member( numeric_id<member> id /* ... */ ) : id_(id) { /* ... */ }
      // ...
      numeric_id<member> id() const { return id_ ; }
      // ...
      private: int id_ ;
      // ..
};

void foo( book& bk, member& m )
{
     // int book_id = bk.id() ; // *** error
     // int member_id = m.id() ; // *** error
     // numeric_id<book> book_id = m.id() ; // *** error

     numeric_id<book> book_id = bk.id() ; // fine
     numeric_id<member> member_id = m.id() ; // fine
     std::cout << "book id is: " << book_id  << '\n' ; // fine

     //int i = book_numeric_id * member_numeric_id  ; // *** error

     //member new_member( book_id /* ... */ ) ; // *** error
     member new_member( member::lookup_id( "member name" /* ... */ ), /*...*/ ) ; // fine
     // do things with member
}


IMHO,

a. The programming language can give a lot of help in avoiding type errors, and it is good if it does. However, in the ultimate analysis, type-safety is the responsibility of the programmer who designs the type.

b. Implicit conversion operators are an invaluable aid to writing seamless type safe code, without sacrificing readability, convenience and performance.
Last edited on
closed account (zb0S216C)
Your assistance is well appreciated, JLBorges :) I feel more knowledgeable.

Thanks again.

Wazzak
Last edited on
Topic archived. No new replies allowed.