1

I was working on a class :

#include <iostream>
#include <list>
using namespace std;

class Int{
    list <int64_t> data;
    bool sign;
public:
    Int(int64_t val = 0) : sign(false) {
        cout << "Int(int) called\n";
    }
    Int(const char* str): sign(false) {
        cout << "Int(const char* called)\n";
    }
};

int main(){
    Int a = "328739";     // line 1, ok
    Int a2 = "ehfjhs";    // line 2, ok
    Int a3 = 4338974;     // line 3, ok
    Int a4 = 0;    //line 4, Issue
    return 0;
}

Everything works fine, except line no 4.

As soon I do Int a4 = 0;, the constructor Int(const char* str) is called, as 0 is equivalent to NULL. But I want Int(int64_t val = 0) to be called instead.

A simple fix I could make was doing Int a4 = int(0); that is ok on a side. But I want to make it flexible, so 0 triggers Int(int64_t val = 0) only.

  • 1
    I would recommend you to check this [thread](https://stackoverflow.com/questions/51391703/how-does-function-overloading-work-when-passing-null-as-argument). – rawrex May 27 '21 at 06:00
  • 1
    This is a fun one. Normally you'd make the constructor `explicit`, but that doesn't work with string literals. String literals convert to `const char *`, but avoiding conversion is what `explicit` is all about. – user4581301 May 27 '21 at 06:03
  • 3
    Add a static member function named like `from_str()` to construct from a string explicitly. All these implicit conversions are no good. – Evg May 27 '21 at 06:11
  • GCC and clang refuse to compile the code at all btw, on account of the ambiguity, –  May 27 '21 at 06:13
  • @rawrex thanks for that reference, that quite a bit clarifies this 0 and NULL thing ambiguity. – Suraj Singh May 27 '21 at 06:18
  • @SurajSingh you're welcome. Glad to help! – rawrex May 27 '21 at 06:19
  • 2
    @SurajSingh that's why NULL shouldn't ever be used instead of nullptr – Moia May 27 '21 at 06:26

2 Answers2

3

As an alternative to the other answer, you can make a template constructor that accepts any integral type. This would resolve the ambiguity and additionally work for any integral type or literal.

#include <iostream>
#include <list>
using namespace std;

class Int{
    list <int64_t> data;
    bool sign;
public:
    template <typename T, std::enable_if_t<std::is_integral_v<T>>* = nullptr>
    Int(T val = 0) : sign(false) {
        cout << "Int(Integral) called\n";
    }
    Int(const char* str): sign(false) {
        cout << "Int(const char* called)\n";
    }
};

int main(){
    Int a = "328739";     // line 1, ok
    Int a2 = "ehfjhs";    // line 2, ok
    Int a3 = 4338974;     // line 3, ok
    Int a4 = 0;           // line 4, ok
    return 0;
}
super
  • 12,335
  • 2
  • 19
  • 29
  • Awesome! I had no idea it could be done that way too. Thanks @super – Suraj Singh May 27 '21 at 16:40
  • Simpler than what I was playing with: `explicit Int(const char* str): sign(false) { cout << "Int(const char* called)\n"; }` to prevent unwanted conversions and then `template Int(const char (&str)[N] ): sign(false) { cout << "string literal called)\n"; }` to handle string literals – user4581301 May 27 '21 at 17:30
2

The key point here is that 0 is not an int64_t, so it has to go through an implicit conversion before being used in either of your constructors. Both constructors are equally valid here. gcc and clang actually flag the ambiguity as an error instead of arbitrarily picking one like you are experiencing.

The type of 0 is int, so if you have an int constructor, it will bind to 0 without going through any implicit conversion beforehand, which resolves the ambiguity:

class Int{
    list <int64_t> data;
    bool sign;
public:
    Int(int64_t val = 0) : sign(false) {
        cout << "Int(int) called\n";
    }
    Int(int val) : sign(false) {
        cout << "Int(int) called\n";
    }
    Int(const char* str): sign(false) {
        cout << "Int(const char* called)\n";
    }
};
  • Thanks man! that resolved the issue. But I wonder why does it not work with int64_t only. I will see it. – Suraj Singh May 27 '21 at 06:20
  • Well@SurajSingh that's what the start of my answer covers. `0` being an `int`, it needs to be casted as a `int64_t` beforehand. However, there is no preference between implicitely casting 0 to a pointer or casting it to int64_t, so the compiler can't tell which constructor it's supposed to use. Modern compilers actually just flat out refuse to compile the code. –  May 27 '21 at 06:22
  • This is more an ad-hoc fix rather than a real solution. – Evg May 27 '21 at 06:23
  • @Evg I disagree. If OP wants their class to be constructible from any `int` literal as well as a pointer type , then having an `int` constructor that binds directly to the literal's type is exactly what they need. –  May 27 '21 at 06:26
  • Then why do you have a ctor that takes `int64_t`? Will this solution work with other integral types? – Evg May 27 '21 at 06:29
  • @evg I assumed OP still wants their int64_t constructor for the class' actual purpose. the issue is only the the `0` *literal*, integral types are fine in general. Pedantically, `0ul`would still cause issues, but that can be treated as an edge-case –  May 27 '21 at 06:30
  • @Evg your logic will also work for me, but I was looking for the compiler to do this by calling the appropriate constructor, so Frank's code does exactly the required thing in my case. Thanks to both of you. – Suraj Singh May 27 '21 at 06:32
  • Try initializing `Int` with `0ll`, for example. It's pretty unexpected that `Int` can't be initialized with `0ll`, isn't it? – Evg May 27 '21 at 06:32