C++ Logo

std-proposals

Advanced search

[std-proposals] Tagging

From: Frederick Virchanza Gotham <cauldwell.thomas_at_[hidden]>
Date: Sat, 19 Apr 2025 23:58:54 +0100
This past week in the thread about "std::arithmetic", we've been
talking about different ways of tagging a class.

Here's a class that has a tag that will be inherited by all derived classes:

    #include <type_traits>

    class MyClass {
    public:
        typedef int tag_arithmetic;
    };

    class MyDerivedClass : public MyClass {

    };

    template<typename T>
    void Func(void)
    {
        static_assert( std::is_same_v<int, typename T::tag_arithmetic> );
    }

    int main(void)
    {
        Func<MyDerivedClass>();
    }

The derived class can opt out of the tag as follows:

    class MyDerivedClass : public MyClass {
    public:
        typedef void tag_arithmetic;
    };

And here's a way of writing a class with a tag which is not implicitly
inherited by derived classes:

    class MyClass {
    public:
        typedef MyClass tag_arithmetic;
    };

    class MyDerivedClass : public MyClass {

    };

    template<typename T>
    void Func(void)
    {
        static_assert( std::is_same_v<T, typename T::tag_arithmetic> );
    }

    int main(void)
    {
        Func<MyDerivedClass>();
    }

And the derived class can opt in as follows:

    class MyDerivedClass : public MyClass {
    public:
        typedef MyDerivedClass tag_arithmetic;
    };

An alternative to using 'typedef' for creating tags is to use a static
member boolean as follows:

    #include <type_traits>

    class MyClass {
    public:
        static constexpr bool tag_arithmetic = true;
    };

    class MyDerivedClass : public MyClass {

    };

    template<typename T>
    void Func(void)
    {
        static_assert( T::tag_arithmetic );
    }

    int main(void)
    {
        Func<MyDerivedClass>();
    }

And the derived class can opt out as follows:

    class MyDerivedClass : public MyClass {
    public:
        static constexpr bool tag_arithmetic = false;
    };

And another way of tagging a class is to use an empty base class as follows:

    class tag_arithmetic {};

    class MyClass : public tag_arithmetic {};

    class MyDerivedClass : public MyClass {};

    template<typename T>
    void Func(void)
    {
        static_assert( std::is_base_of_v<tag_arithmetic, T> );
    }

    int main(void)
    {
        Func<MyDerivedClass>();
    }

(This particular method could get really interesting at runtime with
'dynamic_cast')

In all the ways shown so far of tagging a class, the tag is part of
the class definition. What this means is that if a class is inside a
header file, and if you want to add a tag to that class, then you must
edit the header file. There is another method of tagging though that
allows you to leave the class definition intact, as follows:

    template<typename T>
    constexpr bool tag_arithmetic = false;

    class MyClass {};

    template<>
    constexpr bool tag_arithmetic<MyClass> = true;

    class MyDerivedClass : public MyClass {};

    template<>
    constexpr bool tag_arithmetic<MyDerivedClass> = true;

    template<typename T>
    void Func(void)
    {
        static_assert( tag_arithmetic<T> );
    }

    int main(void)
    {
        Func<MyDerivedClass>();
    }

As you can see in the above code snippet, the tag is not automatically
inherited by derived classes.

I want to discuss the possibility of standardising tagging. I think
any given tag should have the following configurable attributes:
    1) Tag is inherited by default, or not inherited by default
    2) Tag is within class definition, or outside class definition

So here's a tag that's automatically inherited by derived classes:

    class MyClass {
    public:
        _Tag arithmetic;
    };

You can consider the above class definition to be short-hand for:

    class MyClass {
    public:
        _Tag arithmetic = true;
    };

Here's a tag that's not automatically inherited by derived classes:

    class MyClass {
    public:
        _Tag arithmetic : final;
    };

which again is short-hand for:

    class MyClass {
    public:
        _Tag arithmetic : final = true;
    };

Here is how you write a derived class and tell the compiler "This
class has the tag if any of its bases have the tag" -- so that you can
opt into a tag which is marked 'final' in the base class as follows:

    class MyClass {
    public:
        _Tag arithmetic : final;
    };

    class MyDerivedClass : public MyClass {
    public:
        _Tag arithmetic = ?;
    };

And if you don't want to edit the header file for a class, then you
can add the tag after the class definition as follows:

    class MyClass {};
    MyClass << _Tag arithmetic : final;
    class MyDerivedClass : public MyClass {};
    MyDerivedClass << _Tag arithmetic = ?;

And here's how a derived class opts out of a base class's tag:

    class MyClass {
    public:
        _Tag arithmetic;
    };

    class MyDerivedClass : public MyClass {
    public:
        _Tag arithmetic = false;
    };

Note that the expression after the '=' operator can be any constant
expression that evaluates to a boolean, for example:

    class MyClass {
    public:
        _Tag arithmetic = is_debugging_enabled && is_built_as_static_executable;
    };

Now a few of you might be thinking right now:
    So you've shown us 5 different ways of tagging a class, so what's
good about increasing that number to 6? Well here's what I'm thinking:

    Point No. 1: If the 6th tagging system is clearly superior to
the other 5, then people will adopt it and abandon the other 5.
    Point No. 2: Use of the new keyword "_Tag" makes it more
explicit and easier to figure out exactly what the code is doing at a
glance.
    Point No. 3: Once tagging is standardised, the documentation
for an SDK library can have a comment such as "The type T must have
the arithmetic tag", and the programmer doesn't have to scratch their
head wondering which of the 5 tagging systems is used -- instead they
know it's the standardised system.

And finally: The reflection system would allow constexpr iteration
through a class's tags as follows:

    for ( auto const &t = std::get_tags( typeid(T) ) )
    {
        cout << "Tag: " << t.name() << " Inherited from base: " <<
(t.was_inherited() ? "yes" : "no") << endl;
    }

FIN.

Received on 2025-04-19 22:59:03