Reputation: 311
I have this hierarchy (methods are simplified):
struct BaseExpr {
explicit BaseExpr(ExprType expr_type) : expr_type_(expr_type) {}
virtual ~BaseExpr() = default;
template<typename T>
T const* As() const {
if (expr_type_ == T::expr_type) {
return static_cast<T const*>(this);
}
return nullptr;
}
ExprType expr_type_;
};
//CRTP
template<typename T>
struct Expr : public BaseExpr {
Expr() : BaseExpr(T::expr_type) {}
~Expr() override = default;
};
struct Variable : public Expr<Variable> {
static ExprType const expr_type = ExprType::kVariable;
// ...other methods
std::string name_;
};
Sometimes i need to downcast a pointer of a BaseExpr
, to a concrete class, e.g. Variable
, but i don't want to use dynamic_cast
. So i store another static member (expr_type
) for comparison purpose. Is this approach good? Or perhaps there a better of doing such thing?
Upvotes: 0
Views: 339
Reputation: 2233
Using static cast, assuming when you know and are confident of the derivative type is, in my experience, a very effective way of having single inheritance RTTI, without all the string comparisons.
I personally do this so often that I've written macros that handle the cast. Here is a simplified version of it:
// Used in the header of the derived class.
#define RTT_IMPL(TYPE) \
static void* _getStaticRuntimeType(); \
virtual void* _getRuntimeType() const override \
{ \
return TYPE::_getStaticRuntimeType(); \
}
// Used in a translation unit.
#define RTT_IMPL_TU(TYPE) \
void* TYPE::_getStaticRuntimeType() \
{ \
static int magicHandle = 0; \
return &magicHandle; \
} \
// Used in the header of the base class.
#define RTT_BASE_IMPL(TYPE) virtual void* _getRuntimeType() const = 0;
// Used in the header of the base class.
#define RTT_BASE_CHECK(TYPE) \
template<typename T> \
bool _isRuntimeType() const \
{ \
return T::_getStaticRuntimeType() == _getRuntimeType(); \
} \
template<typename T> \
static bool _isRuntimeType(const TYPE& obj) \
{ \
return obj._isRuntimeType<T>(); \
}
// Used in the header of the base class.
#define RTT_BASE_CAST(TYPE) \
template<typename T> \
T* _castRuntimeType() \
{ \
return _isRuntimeType<T>() ? static_cast<T*>(this) : nullptr; \
} \
template<typename T> \
static T* _castRuntimeType(const TYPE& obj) \
{ \
return obj._castRuntimeType<T>(); \
}
#define RTT_CHECK(_VAR, T) (std::remove_pointer_t<std::remove_reference_t<decltype(_VAR)>>::_isRTType<T>(_VAR))
#define RTT_CAST(_VAR, T) (std::remove_pointer_t<std::remove_reference_t<decltype(_VAR)>>::_castRTType<T>(_VAR))
The BASE
macros are placed within the base class, RTT_IMPL
is placed in the derived class definition, and RTT_IMPL_TU
is placed within a translation unit (CPP file).
The RTT_IMPL_TU
being a separate macro is necessary for this to work across shared library boundaries. If you do use this across shared library boundaries you will need to decorate the function declaration in the header with the appropriate compiler attribute (__declspec(dllexport)
, __declspec(dllimport)
, __attribute__((visibility("default")))
, etc).
The magicHandle
can really be anything, we just need a value that will always be unique to that type. In this case because we have it as a static member of a static function its address will remain constant, and that address is what becomes our unique identifier.
This is only the baseline of what I typically use. I also have many wrapper functions that can handle taking in a wide variety of pointer types (raw pointers, shared pointers, weak pointers, etc).
dynamic_cast
dynamic_cast
is implemented using RTTI. There are many good reasons for disabling RTTI, one of them is all the debug strings it leaves around making it easier to reverse an executable. There is also the factor of executable size from these strings. These strings are the key to how dynamic_cast
is typically implemented. Most implementations that I have seen (the Windows x64 ABI, most Linux x64 ABI's, and the Macho ABI) use string comparisons to check the type of the object. This is incredibly slow, especially compared to checking an integer, and then performing a static_cast
(which is mostly at the language level, and at most has some pointer arithmetic to adjust for the derived class).
On Windows with Visual Studio you can find the implementation of dynamic_cast
in your installation folder: 2019/Community/VC/Tools/MSVC/%VERSION%/ctr/src/vcruntime/rtti.cpp
. If the type only has single inheritance it use a simpler casting routine that mostly doesn't rely on string comparisons, but it can still fallback to that. Additionally that routine is designed to handle deep levels of inheritance, where every level is a potential for a cast resut. If you only have single level inheritance, or you don't need to cast to any of the intermediate levels this will be a lot faster.
One answer implies that the cost of VTable and a dynamic_cast
is the same. This is not the case. Calling a virtual function simply requires dereferncing the first member of the object to get the vtable, then dereferencing the member of vtable, and then perform an indirect jump. This is a very fast process on modern CPU's. dynamic_cast
doesn't actually call any virtual functions (and even if it does, thats on top of, not in place of, the other performance implications), it accesses the static RTTI data to know if the type is correct, and then uses that data to compute how much the object pointer needs to be manipulated to correctly point to the derived type.
I could go on for hours about what that quote is actually talking about, but needless to say, optimization isn't bad. Ultimately there is nothing more permanent than a temporary solution, so optimize from the beginning, because it's not going to be done later.
Additionally, dynamic_cast
s implemented with string comparisons have been consistently shown to take up measurable amounts of time. Given OP is writing what appears to be a compiler, a process which can run for very long periods of time during the optimization phase, this is almost certainly a worthwhile optimization, and ultimately doesn't take that much time to implement.
Optimization is balance of performance gained, compared to effort used. If there is a faster way of implementing a function, and it doesn't take substantially more work to implement, I will always opt for the faster method. It doesn't matter if the performance gain is in nanoseconds, by always considering the performance of something I know what to look for when I have to optimize the hotpath.
Upvotes: 0
Reputation: 3272
The 'dynamic_cast' will work in the situation like
class A { public : virtual void foo(); }
class B : public A { }
class C : public B { }
A *base = new C;
B *intermediate =dynamic_cast<B*>(base);
and you need to downcast from A to B while the real object is of type C. And your solution won't, since it recognizes only specific types, not hierarchies.
And it won't work with multiple inheritance inheritance.
Upvotes: 2
Reputation: 13644
Your approach is likely to be more performance efficient, since it is restricted to your scenario.
However you'll have to maintain unique ExprType
values, and also you cannot handle stuff like multi-level hierarchies, not to mention multiple inheritance.
Upvotes: 1
Reputation: 473427
You are already paying for the cost of having a vtable, since you're using virtual functions and the like. So unless performance is a real problem (as evidenced by profiling), if you really, really need to do this (and you should definitely reconsider any code where you need to do this), just use dynamic_cast
. It makes it clear that you're doing a thing that's at least somewhat dubious at the site where you're doing the operation.
Your method works only to the extent that:
You don't use multiple inheritance or virtual
inheritance.
Everyone who creates a new type adds an appropriate value to ExprType
(however that may work, whether an enum or some kind of hash).
Upvotes: 2