How do I avoid implicit conversions on non-constructing functions?
Asked Answered
L

9

91

How do I avoid implicit casting on non-constructing functions?
I have a function that takes an integer as a parameter,
but that function will also take characters, bools, and longs.
I believe it does this by implicitly casting them.
How can I avoid this so that the function only accepts parameters of a matching type, and will refuse to compile otherwise?
There is a keyword "explicit" but it does not work on non-constructing functions. :\
what do I do?

The following program compiles, although I'd like it not to:

#include <cstdlib>

//the function signature requires an int
void function(int i);

int main(){

    int i{5};
    function(i); //<- this is acceptable

    char c{'a'};
    function(c); //<- I would NOT like this to compile

    return EXIT_SUCCESS;
}

void function(int i){return;}

*please be sure to point out any misuse of terminology and assumptions

Limes answered 13/10, 2012 at 22:19 Comment(1)
by the way, the ability to pass a char, long, bool, or basically any other integer type where an int is expected is because of integer promotion and conversion rules that are built into the language. This is a different mechanism than the implicit conversions done with non-explicit constructors.Adim
R
32

8 years later (PRE-C++20, see edit):

The most modern solution, if you don't mind template functions -which you may mind-, is to use a templated function with std::enable_if and std::is_same.

Namely:

// Where we want to only take int
template <class T, std::enable_if_t<std::is_same_v<T,int>,bool> = true>
void func(T x) {
    
}

EDIT (c++20)

I've recently switched to c++20 and I believe that there is a better way. If your team or you don't use c++20, or are not familiar with the new concepts library, do not use this. This is much nicer and the intended method as outlines in the new c++20 standard, and by the writers of the new feature (read a papers written by Bjarne Stroustrup here.

template <class T>
    requires std::same_as(T,int)
void func(T x) {
    //...
}

Small Edit (different pattern for concepts)

The following is a much better way, because it explains your reason, to have an explicit int. If you are doing this frequently, and would like a good pattern, I would do the following:

template <class T>
concept explicit_int = std::same_as<T,int>;

template <explicit_int T>
void func(T x) {

}

Small edit 2 (the last I promise)

Also a way to accomplish this possibility:

template <class T>
concept explicit_int = std::same_as<T,int>;

void func(explicit_int auto x) {

}
Redraft answered 8/4, 2021 at 13:53 Comment(7)
Thank you. I'm switching this to the accepted answer now.Limes
@TrevorHickey Check out my edit, I've been learning more about modern c++ and c++20. Concepts are a great formalization of the benefits that SFINAE give.Redraft
Yes, concepts are good addition to your answer. I've heard it's bad practice to use concepts for constraining a single type, but it's still a valid answer here.Limes
@TrevorHickey I would agree; I wonder what the compiler tax is like when using it too frequently. I put another edit up there which is a much better way to use concepts in this scenario (and generally when restraining types). Interesting syntax.Redraft
I love the abbreviated function template syntax usage in your "Small edit 2" example. Being able to put a bunch of concepts in a common header file and then using them with that simple syntax is great!Raucous
I think the first code chunk should have std::is_same_v<T,int> for C++17 (or, std::is_same<T,int>::value for pre C++17) instead of std::is_same<T,int>.Instinctive
@Instinctive Right you are. I edited it now.Redraft
A
99

Define function template which matches all other types:

void function(int); // this will be selected for int only

template <class T>
void function(T) = delete; // C++11 

This is because non-template functions with direct matching are always considered first. Then the function template with direct match are considered - so never function<int> will be used. But for anything else, like char, function<char> will be used - and this gives your compilation errrors:

void function(int) {}

template <class T>
void function(T) = delete; // C++11 


int main() {
   function(1);
   function(char(1)); // line 12
} 

ERRORS:

prog.cpp: In function 'int main()':
prog.cpp:4:6: error: deleted function 'void function(T) [with T = char]'
prog.cpp:12:20: error: used here

pre-C++11 way:

// because this ugly code will give you compilation error for all other types
class DeleteOverload
{
private:
    DeleteOverload(void*);
};


template <class T>
void function(T a, DeleteOverload = 0);

void function(int a)
{}

[UPDATE 9/24/2023] C++23 version

Since C++23 we might use static_assert(false, msg) to have more clear message to user, due to C++23 fixing DR that allow static_assert of non-value-dependent expressions in a template context:

void function(int) {} // this will be selected for int only

template <class T>
void function(T) {
    // since C++23
    static_assert(false, "function shall be called for int only");
} 
int main() {
    function(1);
    // function(1l);
    // ^^^^^^^^^^^^ produces error:
    // error: static assertion failed: function shall be called for int only
    // 5 |     static_assert(false, "function shall be called for int only");
}

[END UPDATE 9/24/2023]

Analyzer answered 13/10, 2012 at 22:26 Comment(5)
for me the assertions fails even when function is called only with int parametersAdim
@GeoffReedy - static_assert always happens - no matter if instatianted or not. I updated my answer - now it worksAnalyzer
Works great for me. Combined with some macro's, I made my own versions of hton and ntoh functions that will only work with the right type.Broderic
Thank you! I had a worse case, f(string&, string&) and f(bool, string&) where I was forwarding the bool call to the f(string&, string&) case, and I couldn't understand why everything was getting directed to the bool case. After reading this I realized that everything is convertible to bool! The template solution saved my bacon and sanity, and showed the conversion compile error that explained why my string case wasn't getting called.Juba
This answer is perfect. It also seems to work great with constructors as well, preventing the need for "explicit" while still preventing implicit conversions. This means that you can use return {...} without worrying about implicit conversions occurring. This answer is also a lot better than the SFINAE based solutions as those mess around with name mangling while this solution leaves the function signatures alone that are actually allowed.Handicapper
R
32

8 years later (PRE-C++20, see edit):

The most modern solution, if you don't mind template functions -which you may mind-, is to use a templated function with std::enable_if and std::is_same.

Namely:

// Where we want to only take int
template <class T, std::enable_if_t<std::is_same_v<T,int>,bool> = true>
void func(T x) {
    
}

EDIT (c++20)

I've recently switched to c++20 and I believe that there is a better way. If your team or you don't use c++20, or are not familiar with the new concepts library, do not use this. This is much nicer and the intended method as outlines in the new c++20 standard, and by the writers of the new feature (read a papers written by Bjarne Stroustrup here.

template <class T>
    requires std::same_as(T,int)
void func(T x) {
    //...
}

Small Edit (different pattern for concepts)

The following is a much better way, because it explains your reason, to have an explicit int. If you are doing this frequently, and would like a good pattern, I would do the following:

template <class T>
concept explicit_int = std::same_as<T,int>;

template <explicit_int T>
void func(T x) {

}

Small edit 2 (the last I promise)

Also a way to accomplish this possibility:

template <class T>
concept explicit_int = std::same_as<T,int>;

void func(explicit_int auto x) {

}
Redraft answered 8/4, 2021 at 13:53 Comment(7)
Thank you. I'm switching this to the accepted answer now.Limes
@TrevorHickey Check out my edit, I've been learning more about modern c++ and c++20. Concepts are a great formalization of the benefits that SFINAE give.Redraft
Yes, concepts are good addition to your answer. I've heard it's bad practice to use concepts for constraining a single type, but it's still a valid answer here.Limes
@TrevorHickey I would agree; I wonder what the compiler tax is like when using it too frequently. I put another edit up there which is a much better way to use concepts in this scenario (and generally when restraining types). Interesting syntax.Redraft
I love the abbreviated function template syntax usage in your "Small edit 2" example. Being able to put a bunch of concepts in a common header file and then using them with that simple syntax is great!Raucous
I think the first code chunk should have std::is_same_v<T,int> for C++17 (or, std::is_same<T,int>::value for pre C++17) instead of std::is_same<T,int>.Instinctive
@Instinctive Right you are. I edited it now.Redraft
S
27

You can't directly, because a char automatically gets promoted to int.

You can resort to a trick though: create a function that takes a char as parameter and don't implement it. It will compile, but you'll get a linker error:

void function(int i) 
{
}
void function(char i);
//or, in C++11
void function(char i) = delete;

Calling the function with a char parameter will break the build.

See http://ideone.com/2SRdM

Terminology: non-construcing functions? Do you mean a function that is not a constructor?

Subinfeudate answered 13/10, 2012 at 22:22 Comment(8)
non-constructing functions? Do you mean a function that is not a constructor? I doLimes
Since the OP uses C++11, we can =delete it and get a compilation error, not a linker error.Refugio
@ybungalobill c++11 solutions are good. I thought I could only delete functions that are operators inside classes thoughLimes
@LuchianGrigore yes, because of the initializer list syntax for i and cAdim
@ybungalobill you should add that as the answer, it's much better.Subinfeudate
@ybungalobill missed the link you posted . I see it nowLimes
The OP mentioned some other types in addition to char. Possibly the number of undesired actual argument types is unbounded. So the general case would call for a rather large number of function declarations. The "obvious" solution there is to templatize. But then, one removes the possibility of having a general templated overload.Cattish
There is no "automatic promotion" for function arguments, unless we are matching a C-style variadic function. In the first sentence you meant that there is an implicit conversion from char to int.Tangerine
A
8

Here's a general solution that causes an error at compile time if function is called with anything but an int

template <typename T>
struct is_int { static const bool value = false; };

template <>
struct is_int<int> { static const bool value = true; };


template <typename T>
void function(T i) {
  static_assert(is_int<T>::value, "argument is not int");
  return;
}

int main() {
  int i = 5;
  char c = 'a';

  function(i);
  //function(c);

  return 0;
}

It works by allowing any type for the argument to function but using is_int as a type-level predicate. The generic implementation of is_int has a false value but the explicit specialization for the int type has value true so that the static assert guarantees that the argument has exactly type int otherwise there is a compile error.

Adim answered 13/10, 2012 at 22:37 Comment(2)
this is a good solution, but if I do this for a lot of functions, won't I have a lot of structs sitting around in my executable?Limes
No, you shouldn't. No instances of the struct are ever created, it is only used to resolve the constant value.Adim
W
2

Maybe you can use a struct to make the second function private:

#include <cstdlib>

struct NoCast {
    static void function(int i);
  private:
    static void function(char c);
};

int main(){

    int i(5);
    NoCast::function(i); //<- this is acceptable

    char c('a');
    NoCast::function(c); //<- Error

    return EXIT_SUCCESS;
}

void NoCast::function(int i){return;}

This won't compile:

prog.cpp: In function ‘int main()’:
prog.cpp:7: error: ‘static void NoCast::function(char)’ is private
prog.cpp:16: error: within this context
Wimer answered 13/10, 2012 at 22:32 Comment(2)
a fair answer, but I'd perfer to avoid scopingLimes
I think this is the best answer if you just want to prevent one particular type of argument. It works for any version of C++ , involves a lot less typing, and it is an immediate compile time error. That said, the template answers (above) are able to hide all argument types you don't want, but they require C++11.Nonesuch
C
2

For C++14 (and I believe C++11), you can disable copy constructors by overloading rvalue-references as well:

Example: Say you have a base Binding<C> class, where C is either the base Constraint class, or an inherited class. Say you are storing Binding<C> by value in a vector, and you pass a reference to the binding and you wish to ensure that you do not cause an implicit copy.

You may do so by deleting func(Binding<C>&& x) (per PiotrNycz's example) for rvalue-reference specific cases.

Snippet:

template<typename T>
void overload_info(const T& x) {
  cout << "overload: " << "const " << name_trait<T>::name() << "&" << endl;
}

template<typename T>
void overload_info(T&& x) {
  cout << "overload: " << name_trait<T>::name() << "&&" << endl;
}

template<typename T>
void disable_implicit_copy(T&& x) = delete;

template<typename T>
void disable_implicit_copy(const T& x) {
  cout << "[valid] ";
  overload_info<T>(x);
}

...

int main() {
  Constraint c;
  LinearConstraint lc(1);

  Binding<Constraint> bc(&c, {});
  Binding<LinearConstraint> blc(&lc, {});

  CALL(overload_info<Binding<Constraint>>(bc));
  CALL(overload_info<Binding<LinearConstraint>>(blc));

  CALL(overload_info<Binding<Constraint>>(blc));

  CALL(disable_implicit_copy<Binding<Constraint>>(bc));
  // // Causes desired error
  // CALL(disable_implicit_copy<Binding<Constraint>>(blc));
}

Output:

>>> overload_info(bc)
overload: T&&

>>> overload_info<Binding<Constraint>>(bc)
overload: const Binding<Constraint>&

>>> overload_info<Binding<LinearConstraint>>(blc)
overload: const Binding<LinearConstraint>&

>>> overload_info<Binding<Constraint>>(blc)
implicit copy: Binding<LinearConstraint>  ->  Binding<Constraint>
overload: Binding<Constraint>&&

>>> disable_implicit_copy<Binding<Constraint>>(bc)
[valid] overload: const Binding<Constraint>&

Error (with clang-3.9 in bazel, when offending line is uncommented):

cpp_quick/prevent_implicit_conversion.cc:116:8: error: call to deleted function 'disable_implicit_copy'
  CALL(disable_implicit_copy<Binding<Constraint>>(blc));
       ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Full Source Code: prevent_implicit_conversion.cc

Cauchy answered 17/4, 2017 at 19:28 Comment(0)
C
1

Well, I was going to answer this with the code below, but even though it works with Visual C++, in the sense of producing the desired compilation error, MinGW g++ 4.7.1 accepts it, and invokes the rvalue reference constructor!

I think it must be a compiler bug, but I could be wrong, so – anyone?

Anyway, here's the code, which may turn out to be a standard-compliant solution (or, it may turn out that that's a thinko on my part!):

#include <iostream>
#include <utility>      // std::is_same, std::enable_if
using namespace std;

template< class Type >
struct Boxed
{
    Type value;

    template< class Arg >
    Boxed(
        Arg const& v,
        typename enable_if< is_same< Type, Arg >::value, Arg >::type* = 0
        )
        : value( v )
    {
        wcout << "Generic!" << endl;
    }

    Boxed( Type&& v ): value( move( v ) )
    {
        wcout << "Rvalue!" << endl;
    }
};

void function( Boxed< int > v ) {}

int main()
{
    int i = 5;
    function( i );  //<- this is acceptable

    char c = 'a';
    function( c );  //<- I would NOT like this to compile
}
Cattish answered 13/10, 2012 at 23:9 Comment(4)
Oh man - look at my 2 line solution: template <class T> void function(T) = delete;....Analyzer
@PiotrNycz: yes, I saw that - after I wrote the above code. I think it's nifty, nice, but also quite limited, since you cannot have a templated overload. The above would be a more general, reusable solution, if it should turn out to be standard compliant.Cattish
Simple forwarding + decay on the SFINAE check does the trick AFAICT: coliru.stacked-crooked.com/a/ff6323ac11496981. I agree that there is no satisfactory solution (this one will not allow for reference arguments, unless using more wrappers)Brutalize
This comes even close, I suppose: coliru.stacked-crooked.com/a/06a6be3e8f31094eBrutalize
N
1

I first tried PiotrNycz's approach (for C++03, which I'm forced to use for a project), then I tried to find a more general approach and came up with this ForcedType<T> template class.

template <typename T>
struct ForcedType {
    ForcedType(T v): m_v(v) {}
    operator T&() { return m_v; }
    operator const T&() const { return m_v; }

private:
    template <typename T2>
    ForcedType(T2);

    T m_v;
};

template <typename T>
struct ForcedType<const T&> {
    ForcedType(const T& v): m_v(v) {}
    operator const T&() const { return m_v; }

private:
    template <typename T2>
    ForcedType(const T2&);

    const T& m_v;
};

template <typename T>
struct ForcedType<T&> {
    ForcedType(T& v): m_v(v) {}
    operator T&() { return m_v; }
    operator const T&() const { return m_v; }

private:
    template <typename T2>
    ForcedType(T2&);

    T& m_v;
};

If I'm not mistaken, those three specializations should cover all common use cases. I'm not sure if a specialization for rvalue-reference (on C++11 onwards) is actually needed or the by-value one suffices.

One would use it like this, in case of a function with 3 parameters whose 3rd parameter doesn't allow implicit conversions:

function(ParamType1 param1, ParamType2 param2, ForcedType<ParamType3> param3);
Nell answered 23/2, 2018 at 10:11 Comment(0)
P
0

Here's a clean, simple solution in C++11 using enable_if. This example uses std::is_same for the predicate, but you can use any other constexpr yielding bool.

#include <type_traits> // type_traits has enable_if

// take will only accept double, whereas 'normal' functions
// taking double would also accept e.g. float arguments

// compile this with clang++ to get:
// $ clang++ limit*.cc
// limit_argtypes.cc:16:3: error: no matching function for call to 'take'
//   take ( 1.0f ) ;
//   ^~~~
// limit_argtypes.cc:10:6: note: candidate template ignored: requirement
// 'std::is_same<float, double>::value' was not satisfied
// [with arg_t = float]
// void take ( arg_t rhs )
//      ^
// 1 error generated.

// The error message is even clear and concise.

template < typename arg_t ,
           typename = typename std::enable_if
                      < std::is_same < arg_t , double > :: value
                      > :: type
          >
void take ( arg_t rhs )
{ }

int main ( int argc , char * argv[] )
{
  take ( 1.0 ) ;
  take ( 1.0f ) ;
}
Pimentel answered 12/3, 2023 at 9:12 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.