4
\$\begingroup\$

I made some code to get RTTI information of a polymorphic object, it works both if you pass it directly or a pointer to it(the pointer will get dereferenced until null or the object is found), it's going to be used only for debugging log, can this solution be improved in some way?

#include <cstdint>
#include <string>
#include <sstream>
#include <type_traits>
#include <typeinfo>   
#include <utility>
#include <vector>
#include <memory>

class RuntimeInfos final {
private:
    //Prevent external construction, use the static method to use the functionality
    RuntimeInfos() noexcept = default;
    RuntimeInfos(const RuntimeInfos&) noexcept = default;

public:
    //Everything is passed by reference, other templated functions will dispatch them based on the template type's properties
    template<typename T, std::enable_if_t<!std::is_array_v<T>, int> = 0>
    static std::string get_runtime_infos(const T& val) {
        return RuntimeInfos().get_result(val);
    }

private:
    //Utitlities
    template<typename Test, template<typename...> class Ref>
    struct is_specialization : std::false_type {};

    template<template<typename...> class Ref, typename... Args>
    struct is_specialization<Ref<Args...>, Ref> : std::true_type {};

    template<typename T>
    static inline constexpr bool is_smart_ptr_v = is_specialization<T, std::unique_ptr>::value || is_specialization<T, std::shared_ptr>::value || is_specialization<T, std::weak_ptr>::value;

    //Entry point
    template<typename T>
    std::string get_result(const T& val) {
        var_type = typeid(val).name();
        var_address = reinterpret_cast<std::uintptr_t>(&var_type);
        full_chain.push_back(std::make_pair(var_type, var_address));
        exec(val);
        return get_message();
    }

    // A pointer has been passed by reference, copy the pointer and start the job
    template<typename T, std::enable_if_t<std::is_pointer_v<T> && !is_smart_ptr_v<T>, int> = 0>
    void exec(const T& val) {
        T bak = val;
        type_name_runtime(bak);
    }

    // The user has passed a object reference which is ok as we don't need to modify anything in this case
    template<typename T, std::enable_if_t <!is_smart_ptr_v<T> && !std::is_pointer_v<T>, int> = 0>
    void exec(const T& val) {
        return;
    }

    // In the special case of smart pointer it gets the raw pointer and call the correct exec function that will handle that type of pointer
    template<typename T, std::enable_if_t <is_smart_ptr_v<T> && !std::is_pointer_v<T>, int> = 0>
    void exec(const T& val) {
        exec(val.get());
    }

    // This get called if T is a pointer
    template<typename T, std::enable_if_t<std::is_pointer_v<T>, int> = 0>
    void type_name_runtime(T val) {
        is_at_least_level_one_pointer = true;
        ++dereference_count;

        if (val) {
            // Save the current info of the dereferenced val, because if it's not a pointer the job will terminate and we need this info
            points_to = typeid(*val).name();
            points_to_address = reinterpret_cast<std::uintptr_t>(&*val);

            full_chain.push_back(std::make_pair(points_to, points_to_address));

            //This will call itself if the dereference value is still a pointer, else it will call the other template that will end the job
            type_name_runtime(*val);
        }
        else {
            // Since the dereference value is null, use nullptr as if it was a normal value for consistency and simplicity
            points_to = typeid(nullptr).name();
            points_to_address = reinterpret_cast<std::uintptr_t>(nullptr);

            full_chain.push_back(std::make_pair(points_to, points_to_address));

            // Don't call any function, set the flag and exit, job is terminated
            null_ending = true;
        }
    }

    // This get called if T is not a pointer
    template<typename T, std::enable_if_t<!std::is_pointer_v<T>, int> = 0>
    void type_name_runtime(T val) {
        // Job has finished
        return;
    }

    // Give the result, the information is not taken from vector.front() and vector.back()  to give a little bit more of flexibility
    std::string get_message() {
        std::stringstream message;
        message << "(0x" << std::hex << var_address << ") " << "\"" << var_type << "\"";
        if (is_at_least_level_one_pointer) {
            message << " ---> ";
            message << " ... dereference count (" << std::dec << dereference_count << ") ...";
            message << " ---> ";
            message << "(0x" << std::hex << points_to_address << ") \"" << points_to << "\"";
        }
        return message.str();
    }

    // Since pointers are not polymorphic, the chain will contain the same pointer type, with the difference being only the level of the pointer, so it's better to just count and show how many dereference have been made
    std::uintmax_t dereference_count = 0;

    // Information about the type passed to the class
    std::string var_type = "";
    std::uintptr_t var_address = 0;

    // At the end of the job it will contains information about the polymorphic object or about null
    std::string points_to = "";
    std::uintptr_t points_to_address = 0;

    // True if the job has been interrupted because a null pointer has been found, false otherwise. Unused for now
    bool null_ending = false;

    // True if the type passed to the class was at least a pointer, false otherwise
    bool is_at_least_level_one_pointer = false;

    // Contains full chain, unused for now
    std::vector<std::pair<std::string, std::uintptr_t>> full_chain{};
};

Some basic code to test:

        #include <iostream>
        #include <vector>
        #include <memory>

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

        class Derived1 : public BaseClz {
        public:
            Derived1() noexcept = default;
            ~Derived1() noexcept = default;
        };

        class Derived2 : public BaseClz {
        public:
            Derived2() noexcept = default;
            ~Derived2() noexcept = default;
        };

        class DerivedDerived1 : public Derived1 {
        public:
            DerivedDerived1() noexcept = default;
            ~DerivedDerived1() noexcept = default;
        };

        class DerivedDerived2 : public Derived2 {
        public:
            DerivedDerived2() noexcept = default;
            ~DerivedDerived2() noexcept = default;
        };

        class LastDerived : public DerivedDerived1, DerivedDerived2 {
        public:
            LastDerived() noexcept = default;
            ~LastDerived() noexcept = default;
        };

        void do_something_example(BaseClz*** base_clz) {
            std::cout << "Entered do_something function with parameter: " << RuntimeInfos::get_runtime_infos(base_clz) << std::endl;
            try {
                throw std::runtime_error("");
            }
            catch (...) {
                std::cout << "Exception occurred, parameter info: " << RuntimeInfos::get_runtime_infos(base_clz) << std::endl;
            }
        }

        int main() {
            BaseClz* base = new Derived2;
            Derived1* derived1 = new LastDerived;
            BaseClz* derived2 = new Derived2;
            DerivedDerived2* derivedderived2 = new DerivedDerived2;
            BaseClz* base2 = new BaseClz;
            DerivedDerived1* derivderiv1 = new LastDerived;

            BaseClz** ptr = &base;
            BaseClz*** ptr_ptr = &ptr;


            std::vector<BaseClz*> test {base, derived1, derived2, derivedderived2, base2, nullptr};
            std::cout << std::endl;
            for (BaseClz* a : test) {
                std::cout << RuntimeInfos::get_runtime_infos(a) << std::endl;
                std::cout << std::endl;
            }

            std::cout << RuntimeInfos::get_runtime_infos(ptr_ptr) << std::endl;
            std::cout << std::endl;

            do_something_example(ptr_ptr);

            std::cout << std::endl;

            std::unique_ptr<BaseClz> smart_ptr = std::make_unique<DerivedDerived2>();
            std::cout << RuntimeInfos::get_runtime_infos(smart_ptr) << std::endl;

            return 0;
        }

Output will like the following except the address and the rtti string based on the compiler, this is from MSVC:

(0xA) "class BaseClz * __ptr64" --->  ... dereference count (1) ... ---> (0xB) "class Derived2"

(0xC) "class BaseClz * __ptr64" --->  ... dereference count (1) ... ---> (0xD) "class LastDerived"

(0xE) "class BaseClz * __ptr64" --->  ... dereference count (1) ... ---> (0xF) "class Derived2"

(0x10) "class BaseClz * __ptr64" --->  ... dereference count (1) ... ---> (0x11) "class DerivedDerived2"

(0x12) "class BaseClz * __ptr64" --->  ... dereference count (1) ... ---> (0x13) "class BaseClz"

(0x14) "class BaseClz * __ptr64" --->  ... dereference count (1) ... ---> (0x0) "std::nullptr_t"

(0x15) "class BaseClz * __ptr64 * __ptr64 * __ptr64" --->  ... dereference count (3) ... ---> (0x16) "class Derived2"

Entered do_something function with parameter: (0x16) "class BaseClz * __ptr64 * __ptr64 * __ptr64" --->  ... dereference count (3) ... ---> (0x17) "class Derived2"
Exception occurred, parameter info: (0x18) "class BaseClz * __ptr64 * __ptr64 * __ptr64" --->  ... dereference count (3) ... ---> (0x19) "class Derived2"

(0x1A) "class std::unique_ptr<class BaseClz,struct std::default_delete<class BaseClz> >" --->  ... dereference count (1) ... ---> (0x1B) "class DerivedDerived2"
\$\endgroup\$
  • \$\begingroup\$ I don't completely understand what you're trying to do, but could you make use of dynamic_cast? \$\endgroup\$ – sudo rm -rf slash May 13 '20 at 15:03
  • \$\begingroup\$ I'm going to use it for debugging log only, i'm not going to use it to write any logic. I'm going to add an example \$\endgroup\$ – Mirko Angel May 13 '20 at 15:07
  • \$\begingroup\$ Added the example. \$\endgroup\$ – Mirko Angel May 13 '20 at 15:32
  • \$\begingroup\$ Does it de-reference through smart pointers? Pointers themselves are not very common in modern C++ they are usually encapsulated in a memory management object like a smart pointer. \$\endgroup\$ – Martin York May 13 '20 at 16:00
  • \$\begingroup\$ If you get the raw pointer with the get() method it works, i don't think it will be a goog idea to add the option to pass smart pointers, because as i said it's for debugging log, if for example i have an std::unique_ptr i don't want to transfer the ownership to this debugging log function. \$\endgroup\$ – Mirko Angel May 13 '20 at 16:44
1
\$\begingroup\$

This code is really excessively confusing. It seems like basically you're just doing this:

template<class T>
std::string get_runtime_info(const T& t) {
    if constexpr (std::is_pointer_v<T>) {
        if (t == nullptr) {
            using U = decltype(*t);
            return std::string("null pointer to ") + typeid(U).name();
        } else {
            return "pointer to " + get_runtime_info(*t);
        }
    } else {
        return typeid(t).name();
    }
}

but you've surrounded this logic with so much OOP cruft (eight different data members, of a "class" with no public member functions?!) and template metaprogramming (is_specialization_v) that it's hard to tell what's going on.


You've declared your copy constructor noexcept, but it has to copy a bunch of strings and a vector; it is not noexcept. Don't lie to the compiler!

Vice versa, your default constructor is probably noexcept, but since it's explicitly defaulted, I'm pretty sure you don't have to say it's noexcept — the compiler will figure that out on its own. Godbolt agrees.


If you're going to use member functions, make sure you const-qualify the appropriate ones (e.g. get_message() should be const-qualified).


At one point you write std::is_pointer_v<T> && !is_smart_ptr_v<T>, which is redundant; you should just write std::is_pointer_v<T>. Likewise, is_smart_ptr_v<T> && !std::is_pointer_v<T> should just be is_smart_ptr_v<T>.

Since you're doing C++20, you could use constrained templates instead of enable_if:

template<class T> requires std::is_pointer_v<T>
void exec(const T& val) {
    T bak = val;
    type_name_runtime(bak);
}

template<class T> requires is_smart_ptr_v<T>
void exec(const T& val) {
    exec(val.get());
}

template<class T>
void exec(const T& val) {
    return;
}

But it's much better to use a plain old C++17 if constexpr, as in my "simple" rewrite up top:

template<class T>
void exec(const T& val) {
    if constexpr (std::is_pointer_v<T>) {
        T bak = val;
        type_name_runtime(bak);
    } else if constexpr (is_smart_ptr_v<T>) {
        exec(val.get());
    }
}

The line T bak = val; is pointless; C++ copies by default. Since type_name_runtime takes by value (i.e., by copy), therefore there is no observable difference between

        T bak = val;
        type_name_runtime(bak);

and

        type_name_runtime(val);
\$\endgroup\$
  • 1
    \$\begingroup\$ The extra T bak = val can have an observable difference if T's constructor and/or destructor has side-effects. \$\endgroup\$ – G. Sliepen Aug 6 '20 at 8:27

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.