c++implicit-conversionunique-ptr

How to create a unique_ptr while guarding against implicit conversion in ctor?


Here's an example:

class Example { 
public: 
    Example(int m, int n, double pad) {}
    Example(double size, double pad) {}
};

int main() {
    Example example { 1.0, 1.0 };
    auto ptr = std::make_unique<Example>(1.0, 1.0);
}

After removing the parameter pad of the two constructors, the local object example will no longer compile, which is helpful. But ptr still compiles while the meanings of the parameters have changed silently, because of implicit conversion in make_unique:

class Example { 
public: 
    Example(int m, int n) {}
    Example(double size) {}
};

int main() {
    Example example { 1.0, 1.0 }; // Won't compile, which is helpful for reminding me to modify it.
    auto ptr = std::make_unique<Example>(1.0, 1.0); // Still compiles, but parameter meanings are changed silently.
}

I'm not sure if this is a better way:

auto ptr = std::unique_ptr<Example>(new Example { 1.0, 1.0 });

Solution

  • It's easy if you're fine with making the constructor a template

    On C++20 you can use this:

        Example(std::same_as<int> auto m, std::same_as<int> auto n) {}
    

    The equivalent from older standards (that works as well in C++20 if you don't want to make the constructor a template) is adding a "catch-all" template constructor and deleting it.

    class Example { 
    public:
        template<typename T, typename U>
        Example(T, U) = delete;
    
        Example(int m, int  n) {}
        Example(double size) {}
    };
    

    This way, the Example(int, int) overload will be executed only on exact matches of input parameters, all the rest will be "swallowed" by the deleted template constructor; in turn, usage of a deleted template constructor will trigger compilation error.

    EDIT: follow up as per @Jarod42's comment:
    You might be tempted to provide a more generalized deleted constructor.

       template<typename T>
       Example(T&&) = delete;
    

    This might seem to work at first, but it will hinder the autogenerated copy and move constructors, as from this point they will also fall into the "catch-all" category.
    In order to prevent that from happening extra constraints need to be put on the constructor template:

    1. A check if the type argument is also type of the class

    2. And if inheritance comes into play, derived classes also apply here

    There is a number of ways to implement that, the following one is the one I find the easiest:

        template<typename T>
        Example(T&&)
            requires(!std::is_base_of_v<Example, std::remove_cvref_t<T>>) = delete;
    

    https://godbolt.org/z/zr11rfdvb