As you noticed, # has only a certain effect in functional macros. § 6.10.3.2/1 (all references to the standard refer to draft C11 ( N1570 )). To find out what happens in object-oriented macros, we have to look elsewhere.
Form preprocessing directive
# define identifier replacement-list new-line
defines an object-like macro that calls each subsequent instance of the macro name is replaced by a replacement list of preprocessing tokens that make up the rest of the directive. [...]
§ 6.10.3 / 9
So the only question is whether # allowed in replacement-list . If so, he takes part in the replacement, as usual.
We find the syntax in § 6.10 / 1:
replacement-list: pp-tokens (opt.) pp-tokens: preprocessing-token pp-tokens preprocessing-token
Now, # valid preprocessing-token ? Section 6.4 / 1 states:
preprocessing-token: header-name identifier pp-number character-constant string-literal punctuator each non-white-space character that cannot be one of the above
This, of course, is not a header-name (clause 6.4.7 / 1), it is not allowed in identifier tokens (§ 6.4.2.1/1), nor is it a pp-number (which, in principle, is any number allowed format, § 6.4.8 / 1), as well as character-constant (for example, u'c' , § 6.4.4.4/1) or string-literal (exactly what you expect, for example L"String" , § 6.4 .5 / 1).
However, it is indicated as a punctuator in 6.4.6 / 1. Therefore, it is allowed in the replacement-list object-like macro and will be copied verbatim. Now it is re-scanned as described in 6.10.3.4. Let's look at your example:
C(A) will be replaced by C(X#Y) . # here does not have much effect, since it is not in the replacement-list from C , but its argument. C(X#Y) obviously turns into B(X#Y) . Then, argument B converted to a string literal using the # operator in B replacement-list , giving "X#Y"
Therefore, you do not have undefined behavior.