C++ has a feature (I cannot figure out the proper name of it), that automatically calls matching constructors of parameter types if the argument types are not the expected ones.
A very basic example of this is calling a function that expects a std::string
with a const char*
argument. The compiler will automatically generate code to invoke the appropriate std::string
constructor.
I’m wondering, is it as bad for readability as I think it is?
Here’s an example:
class Texture {
public:
Texture(const std::string& imageFile);
};
class Renderer {
public:
void Draw(const Texture& texture);
};
Renderer renderer;
std::string path = "foo.png";
renderer.Draw(path);
Is that just fine? Or does it go too far? If I shouldn’t do it, can I somehow make Clang or GCC warn about it?
3
This is referred to as a converting constructor (or sometimes implicit constructor or implicit conversion).
I’m not aware of a compile-time switch to warn when this occurs, but it’s very easy to prevent; just use the explicit
keyword.
class Texture {
public:
explicit Texture(const std::string& imageFile);
};
As to whether or not converting constructors are a good idea: It depends.
Circumstances in which implicit conversion makes sense:
- The class is cheap enough to construct that you don’t care if it’s implicitly constructed.
- Some classes are conceptually similar to their arguments (such as
std::string
reflecting the same concept as theconst char *
it can implicitly convert from), so implicit conversion makes sense. - Some classes become a lot more unpleasant to use if implicit conversion is disabled. (Think of having to explicitly invoke std::string every time you want to pass a string literal. Parts of Boost are similar.)
Circumstances in which implicit conversion makes less sense:
- Construction is expensive (such as your Texture example, which requires loading and parsing a graphic file).
- Classes are conceptually very dissimilar to their arguments. Consider, for example, an array-like container that takes its size as an argument:
class FlagList { FlagList(int initial_size); }; void SetFlags(const FlagList& flag_list); int main() { // Now this compiles, even though it's not at all obvious // what it's doing. SetFlags(42); }
- Construction may have unwanted side effects. For example, an
AnsiString
class should not implicitly construct from aUnicodeString
, since the Unicode-to-ANSI conversion may lose information.
Further reading:
- The C++ FAQ on explicit constructors
- The Google C++ Style Guide says to nearly always use explicit constructors.
- This StackOverflow question goes into more pros and cons.
This is more of a comment than an answer but too big to put in a comment.
Interestingly, g++
does not let me do that:
#include <iostream>
#include <string>
class Texture {
public:
Texture(const std::string& imageFile)
{
std::cout << "Texture()" << std::endl;
}
};
class Renderer {
public:
void Draw(const Texture& texture)
{
std::cout << "Renderer.Draw()" << std::endl;
}
};
int main(int argc, char* argv[])
{
Renderer renderer;
renderer.Draw("foo.png");
return 0;
}
Produces the following:
$ g++ -o Conversion.exe Conversion.cpp
Conversion.cpp: In function ‘int main(int, char**)’:
Conversion.cpp:23:25: error: no matching function for call to ‘Renderer::Draw(const char [8])’
Conversion.cpp:14:8: note: candidate is: void Renderer::Draw(const Texture&)
However, if I change the line to:
renderer.Draw(std::string("foo.png"));
It will perform that conversion.
4
It’s called implicit type conversion. In general it’s a good thing, as it inhibits unnecessary repetition. For example, you automatically get a std::string
version of Draw
without having to write any extra code for it. It can also aid in following the open-closed principle, as it lets you extend Renderer
‘s capabilities without modifying Renderer
itself.
On the other hand, it’s not without drawbacks. It can make it difficult to figure out where an argument is coming from, for one thing. It can sometimes produce unexpected results in other cases. That’s what the explicit
keyword is for. If you put it on the Texture
constructor it disables using that constructor for implicit type conversion. I’m not aware of a method to globally warn on implicit type conversion, but that doesn’t mean a method doesn’t exist, only that gcc has an incomprehensibly large number of options.