19

I have declared a boost::variant which accepts three types: string, bool and int. The following code is showing that my variant accepts const char* and converts it to bool. Is it a normal behavior for boost::variant to accept and convert types not on its list?

#include <iostream>
#include "boost/variant/variant.hpp"
#include "boost/variant/apply_visitor.hpp"

using namespace std;
using namespace boost;

typedef variant<string, bool, int> MyVariant;

class TestVariant
    : public boost::static_visitor<>
{
public:
    void operator()(string &v) const
    {
        cout << "type: string -> " << v << endl;
    }
    template<typename U>
    void operator()(U &v)const
    {
        cout << "type: other -> " << v << endl;
    }
};

int main(int argc, char **argv) 
{
    MyVariant s1 = "some string";
    apply_visitor(TestVariant(), s1);

    MyVariant s2 = string("some string");
    apply_visitor(TestVariant(), s2);

    return 0;
}

output:

type: other -> 1
type: string -> some string

If I remove the bool type from MyVariant and change it to this:

typedef variant<string, int> MyVariant;

const char* is no more converted to bool. This time it's converted to string and this is the new output:

type: string -> some string
type: string -> some string

This indicates that variant tries to convert other types first to bool and then to string. If the type conversion is something inevitable and should always happen, is there any way to give conversion to string a higher priority?

B Faley
  • 17,120
  • 43
  • 133
  • 223

3 Answers3

13

This has nothing to do with boost::variant, but with the order in which C++ selects the conversions to apply. Before trying to use user-defined conversions (remember that std::string is a user-defined class for this purpose), the compiler will try built-in conversions. There is no built-in conversion from const char* to int, but according to §4.12 in the standard:

A prvalue of [...] pointer [...] type can be converted to a prvalue of type bool.

So the compiler happily converts your const char* to a bool and never gets to consider converting it to a std::string.

UPDATE: It looks like this clearly unwanted conversion is being fixed. You can find a technical explanation of the fix here.

Gorpik
  • 10,940
  • 4
  • 36
  • 56
  • 2
    This just bit me today with a pair of overloaded methods, one which took a `bool`, the other a `const std::string&`. Unfortunate! – davidbak May 25 '16 at 21:11
12

I don't think this is anything particularly to do with boost::variant, it's about which constructor gets selected by overload resolution. The same thing happens with an overloaded function:

#include <iostream>
#include <string>

void foo(bool) {
    std::cout << "bool\n";
}

void foo(std::string) {
    std::cout << "string\n";
}

int main() {
    foo("hi");
}

output:

bool

I don't know of a way to change what constructors a Variant has [edit: as James says, you can write another class that uses the Variant in its implementation. Then you can provide a const char* constructor that does the right thing.]

Maybe you could change the types in the Variant. Another overloading example:

struct MyBool {
    bool val;
    explicit MyBool(bool val) : val(val) {}
};

void bar(MyBool) {
    std::cout << "bool\n";
}

void bar(const std::string &) {
    std::cout << "string\n";
}

int main() {
    bar("hi");
}

output:

string

Unfortunately now you have to write bar(MyBool(true)) instead of foo(true). Even worse in the case of your variant with string/bool/int, if you just change it to a variant of string/MyBool/int then MyVariant(true) would call the int constructor.

Steve Jessop
  • 273,490
  • 39
  • 460
  • 699
  • 9
    To complete your explication: there is an implicit conversion from any pointer type to `bool`, and built-in implicit conversions are always chosen in preference to user defined conversions. (The conversion `char*` to `std::string` counts as user-defined.) As for changing the constructors, you can wrap the class in another class, or derive from it. Depending on context, one of these may or may not be appropriate; both have some drawbacks. – James Kanze Nov 07 '12 at 11:29
  • I think one solution is to remove `bool` from `MyVariant` and use 0 and 1 values instead. – B Faley Nov 07 '12 at 11:35
  • 1
    @Meysam: yes. I thought about recommending that, but then I thought you probably wanted a `MyVariant` initialized with `0` to be different from a `MyVariant` initialized with `false`. If it's OK for them to be the same, just remove `bool`. If they have different meanings, it's not that simple. – Steve Jessop Nov 07 '12 at 11:37
0

I guess what you want to use is a string literal (not const char * itself).

Then, you could try std::string_literals (s-suffix for std::string).

#include <iostream>
#include "boost/variant/variant.hpp"
#include "boost/variant/apply_visitor.hpp"

#include <string>
using namespace std::string_literals; // enables s-suffix for std::string literals

using namespace std;
using namespace boost;

typedef variant<string, bool, int> MyVariant;

class TestVariant
    : public boost::static_visitor<>
{
public:
    void operator()(string &v) const
    {
        cout << "type: string -> " << v << endl;
    }
    template<typename U>
    void operator()(U &v)const
    {
        cout << "type: other -> " << v << endl;
    }
};

int main(int argc, char **argv) 
{
    MyVariant s1 = "some string"s; // use s-suffix ("some string"s is std::string, not const char *
    apply_visitor(TestVariant(), s1);

    MyVariant s2 = string("some string");
    apply_visitor(TestVariant(), s2);

    return 0;
}

Output:

type: string -> some string
type: string -> some string
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
javacommons
  • 113
  • 5