Explicit / Implicit Class Constructors

It's not just overloading that can be a mess. C++ has a bunch of rules about implicit / explicit type conversion for single argument constructors.

For example:

class MagicNumber {
public:
    MagicNumber(int value) {}
};

void magic(const MagicNumber &m) {
  //...
}

int main() {
    //...
    magic(2016);
    return 0;
}

The function magic() takes a const MagicNumber & yet we called it with 2016 and it still compiled. How did it do that? Well our MagicNumber class has a constructor that takes an int so the compiler implicitly called that constructor and used the MagicNumber it yielded.

If we didn't want the implicit conversion (e.g. maybe it's horribly expensive to do this without knowing), then we'd have to tack an explicit keyword to the constructor to negate the behaviour.

explicit MagicNumber(int value) {}

It demonstrates an instance where the default behavior is probably wrong. The default should be explicit and if programmers want implicit they should be required to say it.

C++11 adds to the confusion by allowing classes to declare deleted constructors which are anti-constructors that generate an error instead of code if they match. For example, perhaps we only want implicit int constructors to match but we want to stop somebody passing in a double. In that case we can make a constructor for double and then delete it.

class MagicNumber {
public:
    MagicNumber(int value) {}
    MagicNumber(double value) = delete;
};

void magic(const MagicNumber &m) {
  //...
}

//...
magic(2016);   // OK
magic(2016.0); // error: use of deleted function 'MagicNumber::MagicNumber(double)'

How Rust helps

Rust does not have constructors and so there is no implicit conversion during construction. And since there is no implicit conversion there is no reason to have C++11 style function delete operators either.

You must write explicit write "constructor" functions and call them explicitly. If you want to overload the function you can use Into<> patterns to achieve it.

For example we might write our MagicNumber constructor like this:

struct MagicNumber { /* ... */ }

impl MagicNumber {
  fn new<T>(value: T) -> MagicNumber where T: Into<MagicNumber> {
    value.into()
  }
}

We have said here that the new() function takes as its argument anything that type T which implements the trait Into<MagicNumber>.

So we could implement it for i32:

impl Into<MagicNumber> for i32 {
   fn into(self) {
     MagicNumber { /* ... */ }
   }
}

Now our client code can just call new and providing it provides a type which implements that trait our constructor will work:

   let magic = MagicNumber::new(2016);
   // But this won't work because f64 doesn't implement the trait
   let magic = MagicNumber::new(2016.0);

results matching ""

    No results matching ""