How can I know the implicit conversion used by the compiler?

227 Views Asked by At

In a big project, I have this code, compiling well, but I don't understand why.

std::vector<std::string> foo;
if(foo == 1) // Here is the error, the good code is "if(foo.size() == 1)"
  do_something();

In a simple test main, it doesn't compile. I suppose the compiler finds an operator somewhere and uses it with implicit conversion.

Is there a way to know which one is used ?

I use gcc 8.5.0 with warning options -Wall -Wextra -Wconversion.

In order to understand which kind of code can induce this beheviour, here is an example of a code having such an implicit conversion:

# include <string>
# include <vector>
# include <iostream>

class A
{
    public:
        A() = default;
        A(A const&) = default;
        A(std::vector<std::string> const&) {}
        A(int) {}
};
bool operator==(A const&, A const&) { return true; }

int main()
{
    std::vector<std::string> foo;
    if(foo == 1)
        std::cout << "foo == 1" << std::endl;
    return 0;
}
3

There are 3 best solutions below

5
Caduchon On

Answering my own question

In order to find the source of the implicit conversion, I created another source of implicit conversion. Then, the compiler can't choose and shows the possibilities.

Example:

# include <string>
# include <vector>
# include <iostream>

class A
{
    public:
        A() = default;
        A(A const&) = default;
        A(std::vector<std::string> const&) {}
        A(int) {}
};
bool operator==(A const&, A const&) { return true; }

class B
{
    public:
        B() = default;
        B(B const&) = default;
        B(std::vector<std::string> const& x) {}
        B(int x) {}
};
bool operator==(B const&, B const&) { return false; }

int main()
{
    std::vector<std::string> foo;
    if(foo == 1)
        std::cout << "foo == 1" << std::endl;
    return 0;
}

GCC output:

test_vector.cpp: In function ‘int main()’:
test_vector.cpp:28:9: error: ambiguous overload for ‘operator==’ (operand types are ‘std::vector<std::__cxx11::basic_string<char> >’ and ‘int’)
  if(foo == 1)
     ~~~~^~~~
test_vector.cpp:13:6: note: candidate: ‘bool operator==(const A&, const A&)’
 bool operator==(A const&, A const&) { return true; }
      ^~~~~~~~
test_vector.cpp:23:6: note: candidate: ‘bool operator==(const B&, const B&)’
 bool operator==(B const&, B const&) { return false; }
      ^~~~~~~~
2
Arkanil Paul On

The answer of @Caduchon seems to work in this case, but is incomplete (as @463035818_is_not_an_ai has mentioned).

The scenario that (foo == 1) returns true can be caused due to many operator overloads. But all those can be categorized almost in two groups.

Group 1:

// 1st overload
class A
{
    public:
        A() = default;
        A(A const&) = default;
        A(std::vector<std::string> /* const & */) {}
        A(int /* const & */) {}
};
bool operator==(A const&, A const&) { return true; }

Group 2:

// 2nd overload
bool operator==(std::vector<std::string> /* const & */, int /* const & */) { return true; }

The other overloads can be acheived by modiifying the cv-qualifiers of std::string and int in the above two overloads.

A detailed code for all the overloads is here

But the given two overloads are special.
The presence of the 1st overload will generate ambiguity if any other overload of Group 1 is present (and only if no overload from Group 2 is present).
Similarly, the presence of the 2nd overload will generate ambiguity if any other overload of Group 2 is present.

So, to completely resolve the issue,

  1. Copy the 1st overload in your code and check for ambiguity.
  2. Remove the ambiguity found in step 1.
  3. Remove the 1st overload and copy the 2nd overload and check for ambiguity.
  4. Remove the ambiguity found in step 2.
  5. Remove the 2nd overload.

Now, (foo==1) shall raise an error.

0
Mike Kinghan On

Without introducing new temporary source code to flush out an elusive implicit conversion method, you may bring it to light by generating a verbose, unoptimised assembly listing, with demangled identifiers, of a source file in which the problem implicit conversion is invoked. You will be able to find an explicit invocation of the implicit conversion method in that listing. For example (slightly adapting your posted code):

Suppose a.hpp is the header file, buried in your project, in which the problem implicit conversion is declared.

a.hpp

#ifndef A_HPP
#define A_HPP

# include <string>
# include <vector>

class A
{
    public:
        A() = default;
        A(A const&) = default;
        A(std::vector<std::string> const&) {}
        A(int) {}
};
bool operator==(A const&, A const&) { return true; }

#endif

And main.cpp is a source file in which the problem conversion is invoked.

main.cpp

# include <string>
# include <vector>
# include "a.hpp"

int main()
{
    std::vector<std::string> foo;
    if(foo == 1)
        std::cout << "foo == 1" << std::endl;
    return 0;
}

We can generate a verbose, unoptimised, demangled assembly listing of main.cpp with a command of the form:

$ g++ -S -o - -fverbose-asm [other-option ... ] main.cpp | c++filt > main.txt

Here, g++ -S -o - -fverbose-asm main.cpp writes a verbose unoptimised assembly listing, but with mangled identifiers, to the standard output. Then | c++filt > main.txt pipes it through c++filt, to demangle the identifiers, and directs the output of that to main.txt. (I call the output main.txt, rather than main.s, because with demangled identifiers the listing is no longer well-formed assembly code).

The option -fverbose-asm has the invaluable effect of interjecting the C++ source statements into the assembly instructions that result from them.

Concretely, we may use:

$ g++ -S -o - -fverbose-asm -std=c++17 -Wall -Wextra -pedantic main.cpp | c++filt > main.txt

The resulting main.txt is a large file to survey. But our source editor, or the cat -n command, will give us the source line number of a known invocation of the problem conversion:

$ cat -n main.cpp
 1  # include <string>
 2  # include <vector>
 3  #include <iostream>
 4  # include "a.hpp"
 5  
 6  int main()
 7  {
 8      std::vector<std::string> foo;
 9      if(foo == 1)                               // <- Here!
10          std::cout << "foo == 1" << std::endl;
11      return 0;
12  }

Then we can easily locate in main.txt all of the demangled assembly code generated by that line:

# main.cpp:9:     if(foo == 1)
    leaq    -49(%rbp), %rax #, tmp88
    movl    $1, %esi    #,
    movq    %rax, %rdi  # tmp88,
    call    A::A(int)   #
# main.cpp:9:     if(foo == 1)
    leaq    -48(%rbp), %rdx #, tmp89
    leaq    -50(%rbp), %rax #, tmp90
    movq    %rdx, %rsi  # tmp89,
    movq    %rax, %rdi  # tmp90,
    call    A::A(std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > const&)    #
# main.cpp:9:     if(foo == 1)
    leaq    -49(%rbp), %rdx #, tmp91
    leaq    -50(%rbp), %rax #, tmp92
    movq    %rdx, %rsi  # tmp91,
    movq    %rax, %rdi  # tmp92,
    call    operator==(A const&, A const&)  #
# main.cpp:9:     if(foo == 1)
    testb   %al, %al    # retval.0_9
    je  .L9 #,
    

Here, we see that the first function call generated to compile the line if(foo == 1) is the conversion constructor A::A(int) - the obvious culprit.

A implicit conversion method that is giving us grief need not be an implicit conversion constructor. It might also be an implicit conversion operator that converts its containing class A to some other type B. In that case we would be looking for the likes of

call    A::operator B() const 

in the demangled assembly resulting from a source line where the implicit conversion is invoked.