14

Why does the following code compile in clang++?

Are there any c++ flags to prevent this from happening - I would like the compiler to throw an error because I am passing a std::uint64_t as an argument to a function that accepts std::uint16_t.

#include <cstdint>
using namespace std;

void foo(uint16_t x) {
}

int main() {
    uint64_t x = 10000;
    foo(x);
    return 0;
}
4
  • 3
    Narrowing conversions are permitted in C++, and unsigned narrowing conversions are well-defined Commented Jun 1, 2015 at 20:57
  • 13
    Adding -Werror=conversion will cause your example to not compile. Commented Jun 1, 2015 at 21:06
  • 2
    ..or just -Wall -Wextra -Werror -pedantic Commented Jun 2, 2015 at 11:26
  • 1
    @JohnBollinger Simple: It's error-prone. An uint16_t and an uint64_t are different types. These implicit conversions caused enough headaches. I'd recommend to always set the warning levels high enough to cover such things. Commented Jun 2, 2015 at 15:45

5 Answers 5

33

you can delete a function in c++11

void foo(uint64_t) = delete;

it works by adding the signature at function overload resolution, and if it was a better match, an error occurs. You can also make it generic to allow only you original signature

template <class T> void foo( T&& ) = delete;
Sign up to request clarification or add additional context in comments.

5 Comments

How is this responsive to the question posed?
he asked how to prevent it from happening and this will effectively prevent it.
@JohnBollinger: The given program would fail to compile with this added, would it not?
because it is what he ask, the compiler to throw an error if someone provide an uint64 to the function receiving a uint16
He first asked why his code compiles, which is not addressed in this answer. Fair enough, though, that this does answer the second part.
9

You can also use enable_if as a SFINAE return parameter

#include <iostream>
#include <cstdint>
#include <type_traits>

template<typename T>
typename std::enable_if<std::is_same<T, uint16_t>::value>::type 
foo(T x) 
{
    std::cout << "uint16_t" << std::endl;
}

template<typename T>
typename std::enable_if<!std::is_same<T, uint16_t>::value>::type 
foo(T x)
{
    std::cout << "rest" << std::endl;
}

int main() {
    uint16_t x = 10000;
    uint64_t y = 100000;
    foo(x); // picks up uint16_t version
    foo(y); // picks up anything else, but NOT uint16_t
    return 0;
}

In this way you can have one overload that deals specifically with uint16_t, and another overload that deals with anything else.

2 Comments

This is good but it also prevents widening conversions (as does the other answer). It'd be nice if the function could accept uint8_t values for example, and maybe even literals whose value fits in uint16_t. Someone suggested using std::common_type here however I don't think that would work for uint16_t because of integer promotion to int
@MattMcNabb hmm, this seems a bit more tricky, and it is actually interesting: how to accept anything that "fits" into the parameter. Well, there is a "way": brute force, specify the template explicitly, like foo<int16_t>(x);. Not very pretty though.
6

Here's a solution that would allow widening conversions and prevent the narrowing ones:

#include <cstdint>
#include <type_traits>

void foo(uint16_t x) {
}

template <class T>
typename std::enable_if<sizeof(uint16_t) < sizeof(T)>::type foo(const T& t) = delete;

int main() {
    uint64_t x = 10000;
    uint16_t y = 10000;
    uint8_t z = 100;
    // foo(x); // ERROR: narrowing conversion
    foo(y); // OK: no conversion
    foo(z); // OK: widening conversion
    return 0;
}

In case you'd also like to disallow calls with arguments of signed types (conversions between signed and unsigned types are not "lossless"), you could use the following declaration instead:

#include <cstdint>
#include <type_traits>

void foo(uint16_t x) {
}

template <class T>
typename std::enable_if<(sizeof(uint16_t) < sizeof(T)) ||
                        (std::is_signed<T>::value != std::is_signed<uint16_t>::value)
                       >::type
foo(const T& t) = delete;

int main() {
    uint64_t u64 = 10000;
    uint16_t u16 = 10000;
    uint8_t u8 = 100;
    int64_t s64 = 10000;
    int16_t s16 = 10000;
    int8_t s8 = 100; 

    //foo(u64); // ERROR: narrowing conversion
    foo(u16); // OK: no conversion
    foo(u8); // OK: widening conversion
    //foo(s64); // ERROR: narrowing conversion AND signed/unsigned mismatch
    //foo(s16); // ERROR: signed/unsigned mismatch
    //foo(s8); // ERROR: signed/unsigned mismatch

    return 0;
}

Comments

5

If you want to allow widening conversions, but forbid narrowing conversions, perhaps:

void foo(uint16_t x) {
}

template <class T>
void foo( const T&& t )
{
    return foo(uint16_t{t});
}

This forces all types except uint16_t itself to go through list-initialization, which forbids narrowing conversions.

It doesn't work so well if you already have a number of overloads, though.

10 Comments

Unfortunately narrowing is not required to result in an error, see e.g. stackoverflow.com/q/28466069/3093378. For example, this works: ideone.com/DHqBYL . Maybe I'm missing something.
@vsoftco "if a narrowing conversion (see below) is required to convert the element to T, the program is ill-formed", from 8.5.4
@Barry yes, it is ill formed, however the compiler is not required to emit an error. And in fact compiles the program.
@vsoftco: If you configure your build settings to continue when an ill-formed program is encountered, you get what you ask for. A conforming compiler will never be silent about this, and most will error unless specifically configured not to.
Yes, indeed, you get an warning with -Wall -Wextra on g++. However I found it extremely odd that without enabling warnings you don't get any diagnostic, since I was sure that the program won't even compile.
|
4

Although most answers here are technically correct, you will most likely not want the behaviour to apply only to this function, so a "code level" solution that you have to write for each of these conversion cases is probably not what you want.

On a "project/compilation level" you can add this flag to warn you about these conversions:

-Wconversion

or if you prefer directly treat them as errors:

-Werror=conversion

1 Comment

Note that this won't warn (tested in GCC 4.9.2) about conversion from signed int16_t to unsigned uint16_t, which too can cause invalid value to be passed into function. You can add -Wsign-conversion (or-Werror=sign-conversion) to detect changes in signedness.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.