C++ syntax understanding issue for 'using'

Viewed 3624

I read technical documentation for some projects which are created with C++. I found a line of code that contains syntax I don't understand:

using c = char (& (cClass::* [1]) (cClass(*)[2]) &)[3];

I see the using keyword here. That means we deal with an alias, but what does this line do? How can I understand it? I think this creates a named alias c and assigns the result of the expression on the right to it. But what is in this expression?

4 Answers

Here's how your type is read, step by step. For the sake of clarity, below we declare a variable x.

T x[1]

x is an array (length 1) of type T

cClass::* x[1]

x is an array (length 1) of pointer-to-member inside cClass.

V (cClass::* x[1]) (U)

x is an array (length 1) of pointer-to-member-function inside cClass. Said member function takes U as argument and returns V.

V (cClass::* x[1]) (U) &

x is an array (length 1) of pointer-to-member-function inside cClass. Said member function takes U as argument and returns V, and can only be called on lvalues.

V & (cClass::* x[1]) (U) &

x is an array (length 1) of pointer-to-member-function inside cClass. Said member function takes U as argument and returns reference-to-V, and can only be called on lvalues.

V (& (cClass::* x[1]) (U) &)[3]

x is an array (length 1) of pointer-to-member-function inside cClass. Said member function takes U as argument and returns a reference to array (length 3) of V, and can only be called on lvalues.

Finally, to get your actual type

char (& (cClass::* x[1]) (cClass(*)[2]) &)[3]

we choose V = char, and U = (cClass(*)[2]), the latter being a pointer to array (length 2) of cClass.

More readable alternative:

using char3 = char [3];
using ptrTo2cClass = cClass(*)[2];
using ptrToMethod = char3 & (cClass::*) (ptrTo2cClass) &;
using c = ptrToMethod[1];

Types have to be interpreted starting from the middle (where a variable name would be, if it was a variable declaration). You go the right, until you hit the end or ). Then you go to the left, until you hit the beginning or (. At this point you either interpreted the whole type (then you're done), or you're inside of (...), in which case you discard them and repeat the same procedure (go to the right, then to the left).

Since this is not a variable declaration, you first need to find where the variable name would be if it was one. It's easier to do intuitively (once you get some experience), but a decent rule of thumb is to go from the left until you hit either ), or (...), or [...].

Knowing this, we can turn this into a variable declaration. I've added x as the variable name:

char (& (cClass::*x[1]) (cClass(*)[2]) &)[3];

Now we can start reading the type. First we go to the right:
x Variable x is...
x[1] an array with 1 element, containing...
(Nothing else to the right, go to the left.)
cClass::*x[1] a pointer to member of class cClass, of type...
(cClass::*x[1]) (Nothing else to the left, skip parentheses.)
(cClass::*x[1]) (............) & a function with some parameters, &-qualified1, returning...
(Nothing else to the right, go to the left.)
& (cClass::*x[1]) (............) & a reference to...
(& (cClass::*x[1]) (............) &) (Nothing else to the left, skip parentheses.)
(& (cClass::*x[1]) (............) &)[3] an array with 3 elements, containing...
char (& (cClass::*x[1]) (............) &)[3] chars

And we're finished. The type of the function parameter (cClass(*)[2]) has to be interpreted separately, using the same procedure. It's "a pointer to an array of size 2, containing cClasses".


1 That & to the right of function parameters means it can only be called on lvalues (the function is "&-qualified").

Note: this is an adaptation of my answer at What do the square brackets at the end of this code line mean? and thus I've marked this as Community wiki to avoid getting duplicate reputation points. (Of course, feel free to upvote that answer if you like this one.)

The template based C++ code below lets the compiler itself break down the structure of this declaration for you.

#include <iostream>

template <typename T>
struct introspect;

template <>
struct introspect<char> {
    static std::ostream& prettyPrint(std::ostream& os) { return os << "char"; }
};

template <typename T>
struct introspect<T*> {
    static std::ostream& prettyPrint(std::ostream& os) {
        return os << "pointer to " << introspect<T>::prettyPrint;
    }
};

template <typename T>
struct introspect<T&> {
    static std::ostream& prettyPrint(std::ostream& os) {
        return os << "reference to " << introspect<T>::prettyPrint;
    }
};

template <typename T, typename Res, typename Arg1>
struct introspect<Res (T::*)(Arg1) &> {
    static std::ostream& prettyPrint(std::ostream& os) {
        return os << "pointer to lvalue member function of " << introspect<T>::prettyPrint
                  << " taking (" << introspect<Arg1>::prettyPrint
                  << ") and returning (" << introspect<Res>::prettyPrint << ')';
    }
};

template <typename T, std::size_t N>
struct introspect<T[N]> {
    static std::ostream& prettyPrint(std::ostream& os) {
        return os << "array of " << N << " (" << introspect<T>::prettyPrint << ")";
    }
};

class cClass;
template <>
struct introspect<cClass> {
    static std::ostream& prettyPrint(std::ostream& os) {
        return os << "cClass";
    }
};

int main() {
    using c = char (& (cClass::* [1]) (cClass(*)[2]) &)[3];
    std::cout << introspect<c>::prettyPrint << '\n';
    return 0;
}

Output (with some line breaks added by hand for readability):

array of 1 (pointer to lvalue member function of cClass taking
              (pointer to array of 2 (cClass))
              and returning
              (reference to array of 3 (char)))

After narrowing it down I just saw there already are two answers. Yet I do write a new answer as it shows an alternative how you can narrow it down with STL type traits, going from the outer to the inner.

#include <type_traits>
#include <typeinfo>
#include <iostream>

class cClass {};

template <typename T>
struct Extract : std::false_type {};

template <typename TRet, typename TArg>
struct Extract<TRet (cClass::*)(TArg) &> : std::true_type {
    using Ret = TRet;
    using Arg = TArg;
};

int main()
{
    using Type = char(&(cClass::* [1])(cClass(*)[2])&)[3];
    std::cout << "Type=" << typeid(Type).name() << '\n';
    static_assert(std::is_array_v<Type>);
    using Type1 = decltype(std::declval<Type>()[0]);
    std::cout << "array[" << std::extent_v<Type> << "] of " << typeid(Type1).name() << '\n';
    static_assert(std::is_reference_v<Type1>);
    using Type2 = decltype(std::remove_reference_t<Type1>());
    std::cout << "ref to " << typeid(Type2).name() << '\n';
    static_assert(std::is_member_function_pointer_v<Type2>);
    std::cout << "member function pointer, cClass, taking " << typeid(Extract<Type2>::Arg).name() << ", returning " << typeid(Extract<Type2>::Ret).name() << '\n';

    using FuncParam = cClass(*)[2];                 // Pointer to array of 2 cClass
    using FuncRet = char(&)[3];                     // Reference to array of 3 chars
    using Func = FuncRet(cClass::*)(FuncParam) &;   // Member function for lvalue of cClass, taking FuncParam, returning FuncRet
    using FuncArrayOne = Func[1];                   // Array with one Func (WTF?)
    static_assert(std::is_same_v<FuncArrayOne, Type>);

    return 0;
}

This writes

Type=char (& __ptr64 (__cdecl cClass::*[1])(class cClass (* __ptr64)[2]) __ptr64& )[3]
array[1] of char (& __ptr64 (__cdecl cClass::*)(class cClass (* __ptr64)[2]) __ptr64& )[3]
ref to char (& __ptr64 (__cdecl cClass::*)(class cClass (* __ptr64)[2]) __ptr64& )[3]
member function pointer, cClass, taking class cClass (* __ptr64)[2], returning char [3]

To understand which next function was necessary in each step I output for example:

using Type_x = Type1;
std::cout << std::is_array_v<Type_x> << '\n';
std::cout << std::is_class_v<Type_x> << '\n';
std::cout << std::is_function_v<Type_x> << '\n';
std::cout << std::is_member_function_pointer_v<Type_x> << '\n';
std::cout << std::is_pointer_v<Type_x> << '\n';
std::cout << std::is_reference_v<Type_x> << '\n';

The ones and zeroes give me a hint what to do next.

A tricky part was on the member function pointer. First there is no std type trait to extract the class, the return type and the parameter type(s). And then this was where the & came in place that I failed to understand first. Gladly I knew that you can append & and && to a member function, and the MSVC STL implements _is_member_function_pointer via _Is_memfunptr which gave me an additional hint how and where to add the &.

My code stops here as I now understand all parts. If you wanted to go deeper, you could just repeat the steps.

When going back from inner to outer I added a using declarations in each step, so the code gets much better understandable. Again there was one surprise: the outmost steps suggest that FuncArrayOne should be an array of references to Func, but that is not possible, so it is just an array of Func.

To be sure that the results are correct there is a final static_assert with std::is_same_v.

Related