21

coming from a primarily python background I have somewhat struggled with working with types in C++.

I am attempting to initialise a class variable via one of several overloaded constructors that take different types as parameters. I have read that using the auto keyword can be used for auto declaration of a variable, however in my case it will not be initialised till a constructor is chosen. However the compiler is not happy about not initialising value.

class Token {
public:

    auto value;

    Token(int ivalue) {
        value = ivalue;
    }
    Token(float fvalue) {
        value = fvalue;
    }
    Token(std::string svalue) {
        value = svalue;
    }

    void printValue() {
        std::cout << "The token value is: " << value << std::endl;
    }
};

In python this might look like:

class Token():
        def __init__(self, value):
             self.value = value

        def printValue(self):
             print("The token value is: %s" % self.value)

What is the right way of using the auto keyword in this scenario? Should I use a different approach altogether?

Tom
  • 1,235
  • 9
  • 22
  • 3
    I believe you cannot use `auto` for class members at all? Relevant but outdated question: [Is it possible to have an “auto” member variable?](https://stackoverflow.com/questions/18199728/is-it-possible-to-have-an-auto-member-variable) – Yksisarvinen Dec 18 '19 at 13:46
  • Any reason not to use templates? – Jimmy R.T. Dec 19 '19 at 12:29
  • With python, types are determined on each operation at runtime - that requires overhead, but allows variable types to change from one statement to the next. In C++ the types need to be known in advance, so that code can compile - float and int have different binary layouts and require different assembly instructions to work with. If you want flexibility at runtime, you need to use a union type such as variant that chooses one of many branches containing valid code for each type, adding performance overhead. If you want to keep the int and float versions separate, templates are your friend. – Jake Dec 19 '19 at 12:41

6 Answers6

16

Initialising a variable of unknown type via overloaded constructors in C++

There is no such thing as "variable of unknown type" in C++.

What is the right way of using the auto keyword in this scenario?

auto-deduced variables have a type that is deduced from the initialiser. If there is no initialiser, then you cannot use auto. auto cannot be used for a non-static member variable. One instance of a class cannot have differently typed members than another instance.

There is no way of using auto keyword in this scenario.

Should I use a different approach altogether?

Probably. It looks like you're trying to implement a std::variant. If you need a variable to store one of X number of types, that is what you should use.

However, you may be trying to emulate dynamic typing in C++. While it may be familiar to you due to experience with Python, in many cases that is not the ideal approach. For instance, in this particular example program, all that you do with the member variable is print it. So it would be simpler to store a string in each case. Other approaches are static polymorphism as shown by Rhathin or OOP style dynamic polymorphism as shown by Fire Lancer.

eerorika
  • 232,697
  • 12
  • 197
  • 326
  • `union` is an error-prone low-level mechanism. `variant` probably uses it internally and makes its usage safer. – Erlkoenig Dec 19 '19 at 12:22
  • @wondra union by itself wouldn't be very useful since it cannot be inspected for which member is currently active. It is also very painful to use with non trivial classes (which have custom destructor) such as std::string. What one would want is a *tagged* union. Which is the datastructure that std::variant implements. – eerorika Dec 19 '19 at 12:24
  • 1
    libstdc++'s `variant` [does](https://github.com/gcc-mirror/gcc/blob/892d6439d39997cccf41302187fc56bdbbca1e6f/libstdc%2B%2B-v3/include/std/variant#L333) use `union`. The alternative, using raw memory and placement new, can't be used in a `constexpr` constructor. – Erlkoenig Dec 19 '19 at 12:28
  • @Erlkoenig fair enough, I take back what I said. I had only looked at boosts implementation, which didn't use union, and assumed that everyone did the same. – eerorika Dec 19 '19 at 12:33
11

C++ is a statically typed language, meaning that all variable types are determined before runtime. Therefore, auto keyword is not something like var keyword in javascript, which is a dynamically typed language. auto keyword is commonly used to specify types that are unnecessarily complex.

What you are looking for might be done by using C++ template class instead, which allows creating multiple versions of the class that takes different types.

This code might be the answer you're looking for.

template <typename T>
class Token {
private:
    T value;

public:
    Token(const T& ivalue) {
        value = ivalue;
    }

    void printValue() {
        std::cout << "The token value is: " << value << std::endl;
    }
};

This code would compile if some conditions are met, like the functionoperator<< should be defined for std::ostream& and type T.

KimHajun
  • 148
  • 7
6

Different approach, than what others have proposed, is to use templates. Here is an example:

template<class T>
class Token {
public:

    T value;

    Token(T value) :
        value(std::move(value))
    {}

    void printValue() {
        std::cout << "The token value is: " << value << std::endl;
    }
};

Then you can use your class like this:

Token<int> x(5);
x.printValue();
Rhathin
  • 1,176
  • 2
  • 14
  • 20
3

You can use the std::variant type. The code below shows one way (but it's a bit clumsy, I have to admit):

#include <iostream>
#include <variant>

class Token {
public:

    std::variant<int, float, std::string> value;

    Token(int ivalue) {
        value = ivalue;
    }
    Token(float fvalue) {
        value = fvalue;
    }
    Token(std::string svalue) {
        value = svalue;
    }

    void printValue() {
        switch (value.index()) {
            case 0:
                std::cout << "The token value is: " << std::get<0>(value) << std::endl;
                break;
            case 1:
                std::cout << "The token value is: " << std::get<1>(value) << std::endl;
                break;
            case 2:
                std::cout << "The token value is: " << std::get<2>(value) << std::endl;
                break;
        }
    }
};

int main() {
    Token it(1);
    Token ft(2.2f);
    Token st("three");
    it.printValue();
    ft.printValue();
    st.printValue();
    return 0;
}

It would be much nicer if the std::get<0>(value) could be written as std::get<value.index()>(value) but, alas, the "x" in <x> has to be a compile-time constant expression.

Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
1

auto must be deducible to a specific type, it doesn't provide runtime dynamic typing.

If at the time of declaring Token you know all the possible types you can use std::variant<Type1, Type2, Type3> etc. This is similar to having a "type enum" and a "union". It makes sure proper constructors and destructors get called.

std::variant<int, std::string> v;
v = "example";
v.index(); // 1, a int would be 0
std::holds_alternative<std::string>(v); // true
std::holds_alternative<int>(v); // false
std::get<std::string>(v); // "example"
std::get<int>(v); // throws std::bad_variant_access

An alternative could be to create a different Token subtype for each case (possibly using templates) with suitable virtual methods.

class Token {
public:
    virtual void printValue()=0;
};

class IntToken : public Token {
public:
    int value;
    IntToken(int ivalue) {
        value = ivalue;
    }
    virtual void printValue()override
    {
        std::cout << "The token value is: " << value << std::endl;
    }
}
Fire Lancer
  • 29,364
  • 31
  • 116
  • 182
0

The solution below is similar in spirit to the one in Fire Lancer's answer. It's key difference is that it follows the comment possibly using templates, and thus removes the need to explicitly created derived instances of the interface. Token is not itself the interface class. Instead, it defines the interface as an inner class, and an inner template class to automate the definition of the derived classes.

It's definition appears overly complex. However, Token::Base defines the interface, and Token::Impl<> derives from the interface. These inner classes are entirely hidden to the user of Token. The usage would look like:

Token s = std::string("hello");
Token i = 7;

std::cout << "The token value is: " << s << '\n';
std::cout << "The token value is: " << i << '\n';

Also, the solution below illustrates how one might implement a conversion operator to assign a Token instance to a regular variable. It relies on dynamic_cast, and will throw an exception if the cast in invalid.

int j = i; // Allowed
int k = s; // Throws std::bad_cast

The definition of Token is below.

class Token {

    struct Base {
        virtual ~Base () = default;
        virtual std::ostream & output (std::ostream &os) = 0;
    };

    template <typename T>
    struct Impl : Base {
        T val_;
        Impl (T v) : val_(v) {}
        operator T () { return val_; }
        std::ostream & output (std::ostream &os) { return os << val_; }
    };

    mutable std::unique_ptr<Base> impl_;

public:

    template <typename T>
    Token (T v) : impl_(std::make_unique<Impl<T>>(v)) {}

    template <typename T>
    operator T () const { return dynamic_cast<Impl<T>&>(*impl_); }

    friend auto & operator << (std::ostream &os, const Token &t) {
        return t.impl_->output(os);
    }
};

Try it online!

jxh
  • 69,070
  • 8
  • 110
  • 193