简体   繁体   中英

Use of (templated) deleted function overload to prevent usual arithmetic conversions

I often find that I would like to prevent narrowing or sign conversions (and in general the Usual arithmetic conversions ) for specific constructors or functions. I tend to write:

#include <iostream>

void foo(double f){
    std::cout << "foo double" << f <<std::endl;
}
void foo(float) = delete;
// or template<typename T> void foo(T&& f) = delete;

void bar(unsigned int f){
    std::cout << "bar uint " << f <<std::endl;
}
void bar(signed int ) = delete;
// or template<typename T> void bar(T&& f) = delete;

This does the job...

int main() {
    auto i=2;
    auto d=2.0;
    auto f=2.0f;
    foo(i); // prevented
    foo(d); // OK
    foo(f); // prevented

    auto uil = 3ull;
    auto ul = 3ul;
    auto u = 3u;
    bar(i); // prevented
    bar(d); // prevented
    bar(f); // prevented
    bar(uil); // prevented
    bar(ul); // prevented
    bar(u); // OK
}

Now, is it just a matter of taste in these cases if I use a deleted template or a deleted non-template function, or are there cases where it matters? I find the deleted template more explicit, in preventing all T , but on the other hand, when using this pattern with constructors; forwarding constructors have their issues . In case of the templated version, would it be better to make the deleted template const T& instead?

First, I think it's worth noting that the non-template versions prevent most cases because they cause an ambiguity between the two overloads, while the template ones do it by providing a better match than the non-template overload. Because of this, the error messages generated by the template versions will tend to be clearer, along the lines of "you've tried to call this deleted function", as opposed to "I can't decide between these two, which one do you actually want?". From this point of view, the template version looks better.

However, there are cases where things behave differently.

One obscure case is something like foo({f}); , which is not prevented by the template version, because the initializer list makes the parameter a non-deduced context, so deduction fails for the template, leaving only the non-template overload.

For a similar reason, the template version will block foo({i}); but won't block foo({3}); ( 3 is a constant, so the conversion to double is not a narrowing conversion, because 3 fits in a double and generates the same value when converted back). The non-template version blocks both, because they're both ambiguous.

Another case:

enum E : unsigned { };

int main() 
{
    E e{};
    bar(e);
}

The template version prevents this, by providing the best overload. The non-template one doesn't, because E to unsigned int is a promotion, which is better than E to int , which is a conversion.

Similar issues would appear on platforms where, for example, short is the same size as int , and also for conversions from char16_t , char32_t , or wchar_t , depending on their platform-specific representations.

Although probably less interesting for the context of the question, another difference appears for:

struct A
{
   operator double() { return 7.0; }
};

int main() 
{
   A a{};
   foo(a);
}

The template version prevents this, again by providing the best overload, while the non-template one doesn't (calls foo(double) ).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM