c++c++17abstract-classnamingtypeid

Using typeid in an abstract base class to auto generate derived class names. How to get desired results?


I have this sample program that works but isn't producing the desired results...

Here is the current output:

Output

class Component_00 class Component_01 class Component_02
Successfully connected class Component_01 to class Component_00
Successfully connected class Component_02 to class Component_00
Component class Component_02 already exists in class Component_00!
Successfully connected class Component_02 to class Component_01

And this would be my desired output:

Wire_00 Wire_01 Wire_02
Successfully connected Wire_01 to Wire_00
Successfully connected Wire_02 to Wire_00
Component Wire_02 already exists in Wire_00!
Successfully connected Wire_02 to Wire_01

This is a two-fold question but they are related to the same problem...

The first part is I don't want the word class to be printed before the actual class name.

The second part is I don't want the base class's name to be printed as this is an abstract class. I want the derived class's name to be printed instead...

What do I need to do to resolve this?

-Note- I am using Visual Studio 2017, but this should work agnostic of any compiler used, it should be portable.


Here is all of my relevant source code:

main.cpp

#include <iostream>
#include <exception>
#include "Wire.h"

int main() {
    try {
        Wire w1, w2, w3;
        std::cout << w1.id() << " " << w2.id() << " " << w3.id() << "\n";

        w1.connect(&w2);
        w1.connect(&w3);
        w1.connect(&w3);
        w2.connect(&w3);

    } catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

Component.h

#pragma once

#include <list>
#include <map>
#include <memory>
#include <string>
#include <typeinfo>

class Component {
private:
    std::string id_ = std::string( typeid(*this).name() ) + "_0";
    std::list<Component*> components_;

public:
    Component() {
        updateId();
    }
    virtual ~Component() {}

    std::string& id() { return id_; }

    void connect(Component* other) {
        for (auto& l : components_) {
            if (other->id_ == l->id()) {
                std::cout << "Component " << other->id_ << " already exists in " << id_ << "!\n";
                return;
            }
        }               
        components_.emplace_back( other );
        std::cout << "Successfully connected " << other->id() << " to " << id_ << "\n";
    }

    virtual void propagate() = 0;

private:
    void updateId() {
        static int i = 0;
        id_.append( std::to_string(i++) );
    }
};

Wire.h

#pragma once

#include "Component.h"

class Wire : public Component {
private:

public:
    Wire() {}
    virtual ~Wire() {}

    virtual void propagate() override {
        return;
    }
};


Edit

I modified my base and derived class to use a helper function in generating the class name...

Here are the modified classes:

Component.h

#pragma once

#include <list>
#include <map>
#include <memory>
#include <string>
#include <typeinfo>

template<class T>
constexpr const std::string generateName(T t) {
    return std::string(typeid(t).name());
}

class Component {
protected:
    std::string id_ = "";
    std::list<Component*> components_;

public:
    explicit Component(const std::string& id) : id_{ id } {}
    virtual ~Component() {}

    std::string& id() { return id_; }

    void connect(Component* other) {
        for (auto& l : components_) {
            if (other->id_ == l->id()) {
                std::cout << "Component " << other->id_ << " already exists in " << id_ << "!\n";
                return;
            }
        }               
        components_.emplace_back( other );
        std::cout << "Successfully connected " << other->id() << " to " << id_ << "\n";
    }

    virtual void propagate() = 0;

protected:
    void updateId() {
        static int i = 0;
        id_.append( "_" + std::to_string(i++) );
    }
};

Wire.h

#pragma once

#include "Component.h"

class Wire : public Component {
private:

public:
    Wire() : Component(generateName(this)) {
        updateId();
    };
       
    virtual ~Wire() {}

    virtual void propagate() override {
        return;
    }
};

The main cpp file has not changed...

Here is the new output...

class Wire *_0 class Wire *_1 class Wire *_2
Successfully connected class Wire *_1 to class Wire *_0
Successfully connected class Wire *_2 to class Wire *_0
Component class Wire *_2 already exists in class Wire *_0!
Succesfully connected class Wire *_2 to class Wire *_1

This is now giving me the desired base class's name, however, it is still printing the world class before it in which I don't want, and now it is also appending a space followed by an * in which I also don't want...


Solution

  • The following code (ripped from my RareCpp library) for getting type strings works on VS, gcc, and Clang, as far as I know there is no fully portable solution at this time, typeid makes no guarantee to generate a user friendly type name.

    template <typename T>
    constexpr auto getTypeView()
    {
        std::string_view view;
    #ifdef _MSC_VER
    #ifndef __clang__
        view = __FUNCSIG__;
        view.remove_prefix(view.find_first_of("<")+1);
        view.remove_suffix(view.size()-view.find_last_of(">"));
    #else
        view = __PRETTY_FUNCTION__;
        view.remove_prefix(view.find_first_of("=")+1);
        view.remove_prefix(view.find_first_not_of(" "));
        view.remove_suffix(view.size()-view.find_last_of("]"));
    #endif
    #else
    #ifdef __clang__
        view = __PRETTY_FUNCTION__;
        view.remove_prefix(view.find_first_of("=")+1);
        view.remove_prefix(view.find_first_not_of(" "));
        view.remove_suffix(view.size()-view.find_last_of("]"));
    #else
    #ifdef __GNUC__
        view = __PRETTY_FUNCTION__;
        view.remove_prefix(view.find_first_of("=")+1);
        view.remove_prefix(view.find_first_not_of(" "));
        view.remove_suffix(view.size()-view.find_last_of("]"));
    #else
        view = "unknown";
    #endif
    #endif
    #endif
        return view;
    }
    
    template <typename T>
    struct TypeName
    {
        constexpr TypeName() : value() {
            auto view = getTypeView<T>();
            for ( size_t i=0; i<view.size(); i++ )
                value[i] = view[i];
    
            value[view.size()] = '\0';
        }
        char value[getTypeView<T>().size()+1];
    };
        
    template <typename T>
    std::string TypeToStr() {
        return std::string(TypeName<T>().value);
    }
    

    You can then cut out the struct/class keyword and extra spacing with something like

    std::string simplifyTypeStr(const std::string & typeStr) {
        std::string rawSimpleTypeStr = typeStr;
        if ( rawSimpleTypeStr.find("struct ", 0) != std::string::npos )
            rawSimpleTypeStr.erase(0, strlen("struct "));
        if ( rawSimpleTypeStr.find("class ", 0) != std::string::npos )
            rawSimpleTypeStr.erase(0, strlen("class "));
    
        std::string simpleTypeStr;
        for ( size_t i=0; i<rawSimpleTypeStr.size(); i++ ) {
            if ( rawSimpleTypeStr[i] != ' ' )
                simpleTypeStr += rawSimpleTypeStr[i];
            else if ( ++i < rawSimpleTypeStr.size() ) /* Remove space and upper-case the letter following the space */
                simpleTypeStr += std::toupper(rawSimpleTypeStr[i]);
        }
        return simpleTypeStr;
    }
    

    And cut out the pointer by removing it from the type using std::remove_pointer , your call from generate name would look like...

    template<class T>
    constexpr const std::string generateName(T t) {
        return simplifyTypeStr(TypeToStr<std::remove_pointer_t<T>>());
    }
    

    Copying that in with the rest of your code I got the output...

    Wire_0 Wire_1 Wire_2
    Successfully connected Wire_1 to Wire_0
    Successfully connected Wire_2 to Wire_0
    Component Wire_2 already exists in Wire_0!
    Successfully connected Wire_2 to Wire_1
    

    Edit: be sure to #include <string_view> and make sure your compiler is set to C++17 or higher (in VS this is under Project->Properties, "C++ Language Standard", set that to ISO C++17)