简体   繁体   中英

C++ default arguments can break calling code - how to mitigate?

Disclaimer: I am not a C++ developer, but I hack at a C++ project as a hobby. My question is about understanding some of C++'s design choices and how to manage them as a developer. Note that the project is old and not originally written by me, I just hack it for fun.

Consider the following program:

#include <iostream>
using namespace std;

int addNums(double a, double b)
{
    cout << "addNums impl 1\n";
    return a+b;
}

int addNums(int a, int b, int c)
{
    cout << "addNums impl 2\n";
    return a+b+c;
}

int main()
{
    addNums(1,2);
    return 0;
}

When run, it produces the output "addNums impl 1". But if I now modify the program slightly:

#include <iostream>
using namespace std;

int addNums(double a, double b)
{
    cout << "addNums impl 1\n";
    return a+b;
}

int addNums(int a, int b, int c = 2)
{
    cout << "addNums impl 2\n";
    return a+b+c;
}

int main()
{
    addNums(1,2);
    return 0;
}

It produces the output "addNums impl 2" and the return value of addNums(1,2) has changed (in the first program it's 3, and in the second program it's 5). If we assume the addNums functions are in a library somewhere, and the main part of the program is written by a different person, then the author of the main program has no real way to know what's happened.

In this trivial example it's easy to see what happened, however a similar thing happened with std::string::insert which is distributed as part of gcc: https://www.cplusplus.com/reference/string/string/insert/

Notice in C++11 the second implementation is string& insert (size_t pos, const string& str, size_t subpos, size_t sublen); and then in C++14 the implementation added a default argument to sublen: string& insert (size_t pos, const string& str, size_t subpos, size_t sublen = npos); . As with the trivial example above, programs calling insert with 3 arguments may now suddenly get a different implementation to before. And this different implementation has significantly different behaviour.

As a concrete example, In the project I was working on, some code combines two paths: one is a mount point looks like "/some/mount/" and the other is a path like "/some/path/" with the goal of producing "/some/mount/some/path/". The code uses CStdString.h . A drastically simplified version of it is below:

#include <iostream>
#include "CStdString.h"

using namespace std;

int main()
{
    CStdString cSpath = "/some/path/";
    CStdString cSmount = "/some/mount/";
    cSpath.insert(0, cSmount, cSmount.size()-1);

    cout<<cSpath.c_str();

    return 0;
}

I compile this code using: g++ test.cpp -o test . When this code is compiled using a version of GCC older than 9.1.0, the output is /some/mount/some/path/ but on newer versions the output is //some/path/ . ie, before the default parameter was added, the compiler used an implementation of insert where the 3rd argument represents how many characters from the start of the input string (argument 2) to insert, and the newer version of library causes the compiler to use an implementation where the 3rd argument represents the start position of the input string to insert from. The compiler doesn't give any warnings or errors about this potentially unwanted implementation being used. I tracked the change to this commit in GCC.

My question is, as someone writing or looking at C++ code, how am I supposed to mitigate this kind of thing? It's almost impossible (for someone naive like myself) to figure out why the behvaiour of the program has changed. It seems like GCC has made a breaking backwards incompatible change, and as a developer I have no way to know about it. Are there tools and techniques that seasoned C++ developers use to avoid these problems? If so, what are they?

as someone writing or looking at C++ code, how am I supposed to mitigate this kind of thing? It's almost impossible (for someone naive like myself) to figure out why the behvaiour of the program has changed.

You write unit tests, and you'll know pretty fast the behaviour has changed.

As with all dependencies, upgrading, which necessitates accepting API changes and possible bugs, is not a trivial task. This is why you treat dependencies as a first-class design problem and not as an afterthought.

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