Search code examples
c++c++11constructorassignment-operatorstd-pair

constructor with std::pair as argument: T a({1,2}) works, T a = {1,2} doesn't


I needed big integers for a combinatorics algorithm I'm working on, and as an exercise I thought I'd write a simple 128-bit integer class, but I'm running into some inconsistencies with the constructors.

I have several constructors, so that you can create a uint128_t from another one (with the implicit copy constructor) or from a 64-bit integer or from a pair of 64-bit integers. This all works, but what's confusing me is that I can use this syntax:

uint128_t a = 123ull;
uint128_t b = a;

but not:

uint128_t e = {123ull, 456ull};               // COMPILER ERROR
uint128_t f = std::make_pair(123ull, 456ull); // COMPILER ERROR

even though these work:

uint128_t c({123ull, 456ull});
uint128_t d(std::make_pair(123ull, 456ull));

The errors I'm getting are:

could not convert '{123, 345}' from '<brace-enclosed initializer list>' to 'uint128_t'
conversion from 'std::pair<long long unsigned int, long long unsigned int>' to non-scalar type 'uint128_t' requested

I could just use the syntax that works, but I'm wondering whether there's something simple I'm missing that would get the uint128_t a = {1,2} syntax to work, because that would make it easier to convert existing code to work with 128-bit integers.

Here is an overview of what works and what doesn't, and the relevant parts of the class:

#include "uint128_t.hpp"

int main() {
    uint128_t a = 123ull;               // explixit constructor from uint64_t = ok
    uint128_t b = a;                    // implicit copy constructor = ok
    a = b;                              // assignment from uint128_t = ok
    b = 123ull;                         // assignment from uint64_t = ok
    a = {123ull, 456ull};               // assignment from pair of uint64_t = ok
    b = std::make_pair(123ull, 456ull); // assignment from pair of uint64_t = ok
    uint128_t c({123ull, 456ull});      // explixit constructor from pair = ok
    uint128_t d(std::make_pair(123ull, 456ull));

    uint128_t e = {123ull, 456ull};               // COMPILER ERROR
    uint128_t f = std::make_pair(123ull, 456ull); // COMPILER ERROR

    return 0;
}
#include <cstdint>

class uint128_t {
    private:

    uint64_t hi;
    uint64_t lo;

    public:

    uint128_t() {}
   ~uint128_t() {}
    uint128_t(uint64_t const& val) {
        hi = UINT64_C(0);
        lo = val;
    }
    uint128_t(std::pair<uint64_t const, uint64_t const> const& val) {
        hi = val.first;
        lo = val.second;
    }
    uint128_t const& operator=(uint128_t const&);
    uint128_t const& operator=(uint64_t const);
    uint128_t const& operator=(std::pair<uint64_t const, uint64_t const> const&);
}        
#include "uint128_t.hpp"

uint128_t const& uint128_t::operator=(uint128_t  const& other) {
    this->hi = other.hi;
    this->lo = other.lo;
    return *this;
}

uint128_t const& uint128_t::operator=(uint64_t const val) {
    this->hi = UINT64_C(0);
    this->lo = val;
    return *this;
}

uint128_t const& uint128_t::operator=(std::pair<uint64_t const, uint64_t const> const& val) {
    this->hi = val.first;
    this->lo = val.second;
    return *this;
}

Solution

  • In your first copy-initialization

    uint128_t e = {123ull, 456ull}; 
    

    you are using a multi-valued list to initialize a non-aggregate class. In accordance with the rules of list-initialization, in such cases the compiler will consider std::initializer_list constructors and then it will consider two-parameter constructors in your class. No matching constructor exists in your class, so the initialization fails.

    You probably expected the compiler to convert the {123ull, 456ull} to std::pair and then use that std::pair to initialize e. But list-initialization in C++ does not consider this initialization path. (Note, BTW, that this would also look like a sequence of two user-defined conversions, see below.)

    Your second copy-initialization

    uint128_t f = std::make_pair(123ull, 456ull);
    

    fails for the same reason the following simplified code fails

    struct A { A(int) {} };
    struct B { B(const A &) {} };
    
    int main() {
      B b1(42);  // OK
      B b2 = 42; // Error
    }
    

    The above copy-initialization requires two implicit user-defined conversions in its conversion sequence: from int to A and then from A to B. Even though such conversions exist, implicitly applying two of them is not allowed. At most one implicit user-defined conversion is allowed in copy-initialization.

    In your case you are requesting conversion from std::pair<unsigned long long, unsigned long long> to std::pair<uint64_t const, uint64_t const> (constructor parameter type) and then from std::pair<uint64_t const, uint64_t const> to uint128_t - two conversions. These conversions exist, but two in a row is too many.

    Even if you make sure that arguments of std::make_pair have uint64_t type, those const qualifiers in std::pair<uint64_t const, uint64_t const> will still force the extra conversion. Your make_pair call will produce an std::pair<uint64_t, uint64_t> value which has to be converted to std::pair<uint64_t const, uint64_t const> and then converted to uint128_t.