31

Functions in std

 5 years ago
source link: https://www.tuicool.com/articles/hit/NRFRVfA
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Names of functions from the Standard Library used as pointers/references to functions can cause breakage in your code if you are upgrading to the newer version of C++ or compiling using a different implementation of the Standard Library, or even when just adding headers. In this post we will see why this is so, and what we can do about it.

In C, a function name used without parentheses represents a pointer to the function:

int min(int x, int y) { 
  return x > y ? y : x;
}

int (*funptr)(int, int) = min;

In C++, which inherits form C, this is also the case to some extent, but in general the situation is different, as in C++ we can have function overloads: two or more functions having the same name. Thus, the name alone is no longer enough to say which function you mean. A name like min names not a function but an overload set . You may not see it often as a problem because in some cases the compiler tries to solve the ambiguity for you:

int min(int x, int y) {        // O1
  return x > y ? y : x;
}

int min(int time_in_minutes) { // O2
  return 60 * 1000 * time_in_minutes;
}

int (*funptr)(int, int) = min; // use O1

But there is a limit to how clever a compiler can be:

auto fun = min; // compiler error

This becomes more a practical issue when we start using components, such as the Standard Library, that take functions as parameters:

std::transform(v.begin(), v.end(), v.begin(), min);
// compiler error

The above is not a big deal yet: we can just fix the line, and the program will compile. More unexpected problems occur when you want to pass function names from std :

#include <cctype>
#include <algorithm>
#include <vector>

int main() {
  std::vector<int> s = {'A', 'B'};
  bool b = std::all_of(s.begin(), s.end(), std::isalpha);
}

This program compiles on my machine. It checks if the elements in sequence represent letters and digits. Suppose I want to output the result and/or the sequence. I need to include header <iostream> :

#include <cctype>
#include <algorithm>
#include <vector>
#include <iostream> // new header

int main() {
  std::vector<int> s = {'A', 'B'};
  bool b = std::all_of(s.begin(), s.end(), std::isalpha);
}

And the moment I do it, the program no longer compiles. This is because there are two functions of name isalpha defined in std :

// in <cctype>
int isalpha( int ch );
	
// in <locale>
template< class charT >
bool isalpha( charT ch, const locale& loc );

And it is not obvious (or even specified) which STD header includes which other, so not only adding an STD header might spoil your program, but also switching from one standard library implementation to the other. But this is just the easiest case because both functions are specified in the C++ Standard. Standard Library implementations are allowed to add their own overloads of the required functions, so long as their signatures do not overlap with the formally required ones. In the case of member functions, implementations can additionally define more function parameters than required provided that these parameters can have default values. This means that it is legal for a conforming Standard Library implementation to declare vector::push_back as:

void vector<T>::push_back(const T& elem,
                          size_t times = 1);

And there is even more: the Standard reserves the right to change the signatures and overloads of the existing Standard Library functions in what is considered a backward compatible way. Quoting from P0921r2 , which was accepted at the recent meeting in Rapperswil:

Primarily, the standard reserves the right to:

  • Add new names to namespace std ,
  • Add new member functions to types in namespace std ,
  • Add new overloads to existing functions,
  • Add new default arguments to functions and templates,
  • Change return-types of functions in compatible ways (void to anything, numeric types in a widening fashion, etc),
  • Make changes to existing interfaces in a fashion that will be backward compatible, if those interfaces are solely used to instantiate types and invoke functions. Implementation details (the primary name of a type, the implementation details for a function callable) may not be depended upon.
    • For example, we may change implementation details for standard function templates so that those become callable function objects. If user code only invokes that callable, the behavior is unchanged.

This means that the Standard guarantees what happens when you actually call functions, but not when you pass them by names or addresses.

For instance, there may be just one function toupper in sight:

int c;
int c2 = std::toupper(c);

If in the next release its signature changes from:

int toupper(int ch);

to:

int toupper(int ch, bool ascii = true);

The above code will still compile; so in a sense this is a backward compatible change for “just call the function” usages; but the following code that used to work will now break:

std::transform(v.begin(), v.end(), v.begin(), std::toupper);

Because function pointers do not see default function arguments.

One other, minor problem. The provision that Standard Library functions that return void can be changed in the future to return anything can hit you in a one more way. In C++ you can have an expression of type void in a return statement, provided that the function you are returning from is also declared to return void :

void X::update(optional<int> v)
{
  if (!v) return vec_.clear();

  vec_.push_back(*v);
  // do other stuff
}

This works because member function clear() returns void . But if the newer version of the Standard specifies (in what is considered to be a backward compatible way) that clear() returns the number of cleared elements, the above code will break.

Be prepared

To address the problem let’s first observe one thing. When passing function-like entities to function templates (like STL algorithms), we get a compiler error when passing the name of a function overload set, but not when we pass a function object with overloaded function call operator:

template <typename T>
bool is_odd(const T& v) { return v % 2; }

template <typename T>
struct is_even
{
  bool operator()(const T& v) { return v % 2 == 0; }
};

std::all_of(v.begin(), v.end(), is_odd);  // error
std::all_of(v.begin(), v.end(), is_even); // ok

We already have a tool in the language that would help us to pass a function object to the STL algorithm and at the same time perform a function call (rather than fiddling with function names): lambdas. It comes with some syntactic noise, but we can use it to make the example with is_odd work:

std::all_of(v.begin(), v.end(), [](int c){
  return is_odd(c);
});

In order for this solution to be less noisy, noexcept -friendly and SFINAE-friendly, we can define a macro:

# define LIFT(F) [](auto&&... args)                     \
  noexcept(noexcept(                                    \
    F(std::forward<decltype(args)>(args)...)))          \
  -> decltype(F(std::forward<decltype(args)>(args)...)) \
  { return F(std::forward<decltype(args)>(args)...); }

Sorry if it looks scary. This is how perfect forwarding works inside a lambda. Many people declare similar macros, which indicates that we are missing a language feature. In fact such features have been considered (e.g., P0573r2 ), but there is not enough motivation to move them forward. Anyway, convoluted as it is, the macro allows us to pass function overload sets quite easily:

std::all_of(v.begin(), v.end(), LIFT(is_odd)); // works

The C++ Standards Committee are looking at how to address the problem of passing overload sets to functions. It could look something like:

// NOT C++ (yet)
std::all_of(v.begin(), v.end(), []is_odd);

But for now, the best approach might be to go with the macro.

Finally, for the third problem with returning a void expression, technically a lambda would also help to be prepared for the future:

void X::update(optional<int> v)
{
  if (!v) return []{ vec_.clear(); }();

  vec_.push_back(*v);
  // do other stuff
}

But it looks quite confusing, and maybe it is better to go with the longer, but clear form:

if (!v) {
    vec_.clear();
    return;
  }

Or apply a classical trick:

if (!v) return vec_.clear(), void();

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK