Is it possible to narrow only if needed?

136 Views Asked by At

Suppose I had the following code where bar is defined in another library:

void bar(int x);  /* this declaration comes from some included header file */

void foo(long long y)
{
    return bar(y);
}

To make failures detectable, the GSL instructs me to use gsl::narrow as follows:

void bar(int x);  /* this declaration comes from some included header file */

void foo(long long y)
{
    return bar(narrow<int>(y));
}

My problem is that, suppose I knew void bar(long long x); is likely to appear in future releases of the library, the first version will automatically switch to use it (by deduction rules), whereas the second version will not. Is there any way to detect narrowing failures while void bar(long long x); is not available, and switch to just calling it when released?

I tried:

  • bar(narrow(y));, but the template argument of narrow could not be deduced.
  • bar(superint{y}), where superint is a struct {long long x;} with overloaded typecast operator for both long long and int, the latter with narrow check. This is my best idea so far as it gives an ambiguous call compile-time error when void bar(long long x); is added, making us aware of the places to update our codebase when time is right. It doesn't look like something you would put in the GSL though, so I am not quite satified..

UPDATE

There are plenty of good answers, but they all have to be implemented per-function rather than per-argument. Ideally the solution should look something like the GSL where you just do something to the arguments that are in risk of narrowing. The visitor pattern could be useful here, where we would just have to rewrite quz(xi,yi,z,w) to superint_visit(quz, superint{xi},superint{xi},z,w); assuming xi and yi were the integer arguments in risk of narrowing. Something like:

struct superint
{
    long long x;
    operator long long() { return x; }
    operator int()       { return narrow<int>(x); }
};

template <typename F, typename... Args>
auto superint_visit(F func, Args&&... args)
{
    /* if replacing 'superint' with 'long long' in Args 
       gives a function that is declared, call that function 
       (using static_cast<long long> on all 'superint' args 
       to resolve ambiguity). Otherwise, use static_cast<int> 
       on all 'superint' args and call that function instead. */
}

All the above could live in gsl namespace, and I would just have to write my function as:

void foo(long long x)
{
    return superint_visit(bar, superint{x});
}

Even though I accepted an answer here, I'll still like to hear from anyone who can make the above happen!

4

There are 4 best solutions below

2
dfrib On BEST ANSWER

You can leverage the fact that overload resolution favours a perfect argument type match for a non-template function over a function template:

#include <iostream>

// (A)
void bar(int y) { std::cout << "bar(int)\n"; }
// (B)
//void bar(long long) { std::cout << "bar(long long)\n"; }

// (C)
template <typename T>
void bar(T) = delete;

// (C.SP1)
template<>
void bar<long long>(long long y) { bar(narrow<int>(y)); }

int main() {
    long long a = 12LL;
    bar(a); // bar(int)
            // bar(long long) if `bar(long long)` above is available.
}

If void bar(long long); is not available, any call to bar(a) for an argument a of type long long will favour the non-narrowing function template overload (C), whose primary template has been deleted to only allow invocation when T is exactly long long (no conversions) through the specialization (C.SP1). Once void bar(long long); at (B) becomes available, it will be chosen as a better viable candidate by overload resolution than that of the function template candidate.

If you are worried that introducing an additional bar overload (the function template above) may break overload resolution of bar when compiling the library itself, you could add a public wrapper for bar and place the overload resolution delegation above in the TU where the wrapper is defined, applying internal linkage for the added bar overload by declaring it in an unnamed namespace. E.g.:

// foo.h
#pragma once
void foo(long long y);

// foo.cpp
#include "foo.h"
#include "gsl/gsl_narrow"
#include "the_lib/bar.h"

namespace {

template <typename T>
void bar(T) = delete;

template<>
void bar<long long>(long long y) { bar(narrow<int>(y)); }

}  // namespace

void foo(long long y) {
    bar(y);
}
0
StPiere On

There are really nice creative solutions already posted to the question. But they all have the problem of depending of the library implementation - for ex. if bar overloads are present then client code doesnt compile.

From my point of view, what the client of bar really wants in this case is:

"I'd like to use the bar accepting one specific integral parameter, unrelated if other overloads exist".

The library-independent non-intrinsic straight forward way is also the simplest one - a thin bar wrapper accpeting an integral parameter. This way it's undependent of any library implementation details:

void bar(int x);  /* this declaration comes from some included header file */

template <typename T, typename = std::enable_if_v<std::is_integral_v<T>>>
inline void mybar(T x)
{
     // express directly which bar overload you want to use here 
     bar(narrow<int>(x));  // <- the only place you need to ajudst if bar interface changes, btw. use the overload from bar which you really want
}

void foo(long long y)
{
    return mybar(y);  // use mybar instead of bar within your code
}

If bar overloads for int and long are present, then you could differentiate between those two by simply specializing your own mybar.

If bar library interface changes, the client should addapt and recompile anyway. What you really want here is to keep those changes central.

In my opinion, you have already answered your own question, more or less.

8
Bathsheba On

You don't own bar? No problem. It's a matter of getting the decltype for x. This you can do by getting the decltype for the whole function, and writing a template to recover the argument type:

template <typename>
struct argType;

template <typename R, typename A>
struct argType<R(A)>
{
    using type = A;
};

void foo(long long y)
{     
    bar(narrow<typename argType<decltype(bar)>::type>(y));
}
2
Łukasz Ślusarczyk On

Getting type of first argument may be done using boost::callable_traits::args which will give you a std::tuple with types of all arguments, which then may be used to get type of first argument using std::tuple_element

#include <boost/callable_traits/args.hpp>

void bar(int y);

void foo(long long y)
{
  bar(narrow<std::tuple_element<0, boost::callable_traits::args_t<decltype(bar)>>::type>(y));
}