4
\$\begingroup\$

I am using the following code to get a string that is associated with the class of an instance:

std::string gate = 
    dynamic_cast<AND*>(pComponent) != nullptr ? "AND" :
    dynamic_cast<NAND*>(pComponent) != nullptr ? "NAND" :
    dynamic_cast<NOR*>(pComponent) != nullptr ? "NOR" :
    dynamic_cast<NOT*>(pComponent) != nullptr ? "NOT" :
    dynamic_cast<OR*>(pComponent) != nullptr ? "OR" :
    dynamic_cast<XNOR*>(pComponent) != nullptr ? "XNOR" :
    dynamic_cast<XOR*>(pComponent) != nullptr ? "XOR" : ""
;

This works, but I feel like there should be a better way to do this. Perhaps create some kind of map with a reference to the class type of pComponent as key and the related string as value?

All classes (AND, NAND, etc) inherit the same base class, Node. I have been thinking about adding the following code to Node:

virtual std::string get_gate() const = 0;

Then have every derived class implement the method, returning its string and call:

pComponent->get_gate();

Would this solution use memory for every class instance rather than once per class? For example I declare two objects of AND, would both of them allocate memory for the method? Will the compiler do something smart?

\$\endgroup\$
  • 2
    \$\begingroup\$ If a class or any of its ancestors has a virtual function, you get a vptr in each object of that class, so there's overhead compared to an object with no virtual functions at all. Once you have at least one virtual function in the class or an ancestor, adding another virtual function won't add any per-object overhead (at least with the usual way of implementing C++). \$\endgroup\$ – Jerry Coffin Aug 8 '17 at 21:19
  • \$\begingroup\$ I see that you tagged this question as compiler. Could you tell us more about the language that this code helps to compile? \$\endgroup\$ – 200_success Aug 9 '17 at 0:07
  • 1
    \$\begingroup\$ Just use a virtual function, chances are even if you end up paying the cost of the vtable it's only 4 or 8 bytes per object to add a vtable and to be honest most likely it will not matter and you probably already have a vtable if you are deleting by base class pointer. Don't worry about it. \$\endgroup\$ – Emily L. Aug 9 '17 at 0:16
  • \$\begingroup\$ @200_success I added this tag because I figured users interested in compilers know or want to know whether the compiler optimises the code to use less memory per object. \$\endgroup\$ – oddRaven Aug 9 '17 at 9:44
3
\$\begingroup\$

Would this solution use memory for every class instance rather than once per class?

Depends on whether your class already contains a vtable pointer or not.

If it does, there will be no additional memory requirements per instance (just another entry in the vtable, which is shared by all instances).

If it doesn't, each class will need an additional vtable pointer (that will be automatically inserted by the compiler to make virtual dispatch work). Also, a vtable will be generated, but those are one per class.

However, since you are already using (and that includes possibly deleting) instances of derived classes via pointer to base class, the base classes destructor (in this case Node::~Node()) should already be marked virtual so the derived classes destructor gets called correctly when deleting the Node*. This means each class should already have a vtable anyways - so no additional per-instance overhead.

\$\endgroup\$
  • 1
    \$\begingroup\$ Also note, for dynamic_cast or typeid tricks, the compiler either needs to be able to prove the type at compile-time or fall back to rather complex constructs (possibly requiring per-instance memory, though not necessarily in the objects themselves). So there will be a cost to pay in any case. You could try templates, but then you'd have other problems (like exploding compilation times, or nesting limits, or ...). \$\endgroup\$ – hoffmale Aug 8 '17 at 23:10
  • \$\begingroup\$ Neither dynamic_cast nor typeid require "per-instance" memory. Per-class-type, yes; but not per-instance. Not in the objects themselves, and not elsewhere. Nowhere. I hope this clears that up. :) \$\endgroup\$ – Quuxplusone Aug 9 '17 at 3:11
  • \$\begingroup\$ @Quuxplusone: typeid and dynamic_cast require a vtable pointer in order to get the type_info/check whether a downcast to Derived* is possible. This vtable pointer is "per-instance" memory on polymorphic classes. dynamic_cast is kinda cheeky, as it fails at compile time if there's no vtable ("use static_cast instead"). typeid silently does the "wrong thing" (it returns the type_info of the current representation) if there's no vtable. Ergo, if your class doesn't have a vtable, those tricks won't work. To fix that, you'd need to introduce a vtable = per-instance memory. \$\endgroup\$ – hoffmale Aug 9 '17 at 3:47
  • \$\begingroup\$ Just try class Base{}; class Derived : public Base {}; int main() { Derived d; Base *b = &d; if(dynamic_cast<Derived*>(b) != nullptr) /*compile error*/ {} std::cout << typeid(*b).name(); /* prints type of Base, not Derived */ } \$\endgroup\$ – hoffmale Aug 9 '17 at 3:58
1
\$\begingroup\$

Why not:

class Gate
{
    virtual void print(std::ostream& out = std::cout) = 0;
    friend std::ostream& operator<<(std::ostream& stream, Gate const& gate)
    {
        gate->print(stream);
        return stream;
    }
};
class AndGate
{
    virtual void print(std::ostream& out = std::cout) override {out << "AND";}
    .... Stuff
};
.... etc

int main()
{
    AndGate  gate(g1, g2);
    std::cout << gate << "\n";
}
\$\endgroup\$
  • \$\begingroup\$ It might be inconvenient to have to stream into a std::ostringstream every time you need the value as a string! \$\endgroup\$ – Toby Speight Aug 9 '17 at 12:01
  • \$\begingroup\$ @TobySpeight: Thats how boost::lexical_cast<> works. \$\endgroup\$ – Martin York Aug 9 '17 at 20:27
  • \$\begingroup\$ Ah, thanks - I probably ought to take a tourist trip round Boostland sometime soon! \$\endgroup\$ – Toby Speight Aug 9 '17 at 20:35
1
\$\begingroup\$

The simplest solution is to do exactly what you suggested. Starting from:

std::string gate = 
    dynamic_cast<AND*>(pComponent) != nullptr ? "AND" :
    dynamic_cast<NAND*>(pComponent) != nullptr ? "NAND" :
    dynamic_cast<NOR*>(pComponent) != nullptr ? "NOR" :
    dynamic_cast<NOT*>(pComponent) != nullptr ? "NOT" :
    dynamic_cast<OR*>(pComponent) != nullptr ? "OR" :
    dynamic_cast<XNOR*>(pComponent) != nullptr ? "XNOR" :
    dynamic_cast<XOR*>(pComponent) != nullptr ? "XOR" : ""
;

You want a virtual method pComponent->get_gate() that evaluates to "AND" in one case, "NAND" in another, and so on. What is the type of "AND", "NAND", and so on? It's const char *. (Yes, yes, technically it's char[4] and char[5] respectively, but not for our purposes here. :)) So:

class Node {
public:
    virtual const char *get_gate() const { return ""; }
};

class AND : public Node {
public:
    const char *get_gate() const override { return "AND"; }
};

std::string gateName(Node *pComponent) {
    return pComponent->get_gate();
}

However, there's an even simpler approach, if you just want an identifying string and don't care that it's implementation-defined. Try this:

class Node {
public:
    virtual ~Node() = default;
};

class AND : public Node {};

std::string gateName(Node *pComponent) {
    return typeid(*pComponent).name();
}

On Linux/Unix/OS X platforms using Clang or GCC, this gives the strings "4Node" and "3AND"; that is, it gives the mangled class names. On Visual Studio, it gives the strings "class Node" and "class AND".

Using a virtual function get_gate will be marginally faster than the typeid approach (by literally 1 or 2 machine instructions). So it's just a question of how quick-and-dirty you want to be. Since you care enough to ask here, you should probably spend the extra time and lines-of-code to do the get_gate approach; it'll be easier to understand down the road.


Even sticking with the multi-way-branch approach, you'll find that it's much faster to use typeid than to use dynamic_cast, on current library implementations.

std::string gate = 
    typeid(*pComponent) == typeid(AND) ? "AND" :
    typeid(*pComponent) == typeid(NAND) ? "NAND" :
    typeid(*pComponent) == typeid(NOR) ? "NOR" :
    typeid(*pComponent) == typeid(NOT) ? "NOT" :
    typeid(*pComponent) == typeid(OR) ? "OR" :
    typeid(*pComponent) == typeid(XNOR) ? "XNOR" :
    typeid(*pComponent) == typeid(XOR) ? "XOR" : ""
;

This has different behavior from the original if for example NAND inherits from AND; but I'm sure from context that that's not the case.


This would be a decent use for X-Macros.

std::string gateName(Node *pComponent) {
    return (
#define X(K) typeid(pComponent) == typeid(K) ? #K :
#include "gate-types.h"
#undef X
        ""
    );
}
\$\endgroup\$
  • \$\begingroup\$ Why use char* in C++? Wouldn't std::string be a more natural choice? \$\endgroup\$ – user1118321 Aug 9 '17 at 2:15
  • \$\begingroup\$ Short answer: No. Longer answer: std::string does heap allocation, and we don't need that here. \$\endgroup\$ – Quuxplusone Aug 9 '17 at 3:09
  • \$\begingroup\$ std::string only does heap allocations for large strings (>12 bytes, maybe more, depending on implementation). \$\endgroup\$ – hoffmale Aug 9 '17 at 3:52
  • \$\begingroup\$ Why not std::string_view? \$\endgroup\$ – Maikel Aug 9 '17 at 7:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.