In case you need an expandable empty-test which does without e-TeX-extensions and without forbidden tokens, I can offer this one:
%%-----------------------------------------------------------------------------
%% Check whether argument is empty:
%%.............................................................................
%% \CheckWhetherEmpty{<Argument which is to be checked>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked is empty>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked is not empty>}%
%%
%% The gist of this macro comes from Robert R. Schneck's \ifempty-macro:
%% <https://groups.google.com/forum/#!original/comp.text.tex/kuOEIQIrElc/lUg37FmhA74J>
%%
%% Due to \romannumeral-expansion the result is delivered after two
%% expansion-steps/after two "hits" by \expandafter.
\chardef\stopromannumeral=`\^^00
\long\def\firstoftwo#1#2{#1}%
\long\def\secondoftwo#1#2{#2}%
\long\def\CheckWhetherEmpty#1{%
\romannumeral\expandafter\secondoftwo\string{\expandafter
\secondoftwo\expandafter{\expandafter{\string#1}\expandafter
\secondoftwo\string}\expandafter\firstoftwo\expandafter{\expandafter
\secondoftwo\string}\expandafter\stopromannumeral\secondoftwo}%
{\expandafter\stopromannumeral\firstoftwo}%
}%
Like anything else that works in terms of macros, this does not work with arguments that contain \outer-tokens.
Deviating from the requirements formulated in the question, \CheckWhetherEmpty is rather slow.
I take \CheckWhetherEmpty for a moot thing/for a slow workaround in situations where one can't take for granted that e-TeX's \detokenize is available/is allowed by the terms of the macro-writing-challenge.
I emphasize that the gist/the basic idea of "hitting" either the first token of the non-empty argument or the closing brace behind the empty argument with \string for the sake of probably "neutralizing" some braces before it comes to brace-matching in the course of gathering and removing a (to-be brace-balanced) macro-argument, and this way cranking out the brace-matching-cases, does not come from me but does come from Robert R. Schneck's \ifempty-macro.
I just added \romannumeral-expansion and stringification and removal of superfluous curly braces via \expandafter\secondoftwo\string in favor of removing superfluous curly braces via \iffalse..\fi.
I did so for ensuring that things won't break half-way through the expansion-chain due to unbalanced \if..\else..\fi at some stage popping up that might be contained in the argument or might come into being due to "hitting" the first token of the argument with \string...
Besides this the user-provided macro-argument in any stage of the expansion-cascade either is wrapped in a pair of matching curly braces or is already removed. This way the user-provided argument containing & or the like won't disturb the expansion-cascade in case the test is executed inside an alignment or tabular-environment or the like.
In order to explain how the test works, let's rewrite this with different line-breaking:
\long\def\CheckWhetherEmpty#1{%
\romannumeral
\expandafter\secondoftwo\string{%
\expandafter\secondoftwo % <- The interesting \secondoftwo
\expandafter{% <- Opening brace of interesting \secondoftwo's first argument.
\expandafter{%
\string#1}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is an opening brace (Scenario 1).
\expandafter
\secondoftwo\string}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is not an opening brace (Scenario 2).
\expandafter\firstoftwo\expandafter{\expandafter
\secondoftwo\string}%
\expandafter\stopromannumeral\secondoftwo}% <- Closing brace of interesting \secondoftwo's first argument in case #1 is empty (Scenario 3).
{\expandafter\stopromannumeral\firstoftwo}%
}%
The comments about closing braces of "the interesting \secondoftwo" indicate that there are three interesting scenarios.
Let's look at these three scenarios:
Scenario 1: #1 is not empty and #1's first token is an opening brace—e.g., #1={foo}bar:
\CheckWhetherEmpty{{foo}bar}{empty}{not empty}%
Step 1: Toplevel-expansion of \CheckWhetherEmpty delivers the following tokens to TeX's gullet:
\romannumeral
\expandafter\secondoftwo\string{%
\expandafter\secondoftwo % <- The interesting \secondoftwo
\expandafter{% <- Opening brace of interesting \secondoftwo's first argument.
\expandafter{%
\string{foo}bar}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is an opening brace (Scenario 1).
\expandafter
\secondoftwo\string}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is not an opening brace (Scenario 2).
\expandafter\firstoftwo\expandafter{\expandafter
\secondoftwo\string}%
\expandafter\stopromannumeral\secondoftwo}% <- Closing brace of interesting \secondoftwo's first argument in case #1 is empty (Scenario 3).
{\expandafter\stopromannumeral\firstoftwo}%
{empty}{not empty}%
Step 2: \romannumeral-expansion initiated:
%\romannumeral-expansion in progress:
\expandafter\secondoftwo\string{%
\expandafter\secondoftwo % <- The interesting \secondoftwo
\expandafter{% <- Opening brace of interesting \secondoftwo's first argument.
\expandafter{%
\string{foo}bar}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is an opening brace (Scenario 1).
\expandafter
\secondoftwo\string}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is not an opening brace (Scenario 2).
\expandafter\firstoftwo\expandafter{\expandafter
\secondoftwo\string}%
\expandafter\stopromannumeral\secondoftwo}% <- Closing brace of interesting \secondoftwo's first argument in case #1 is empty (Scenario 3).
{\expandafter\stopromannumeral\firstoftwo}%
{empty}{not empty}%
Step 3: \expandafter "hits" \string and { gets stringified:
%\romannumeral-expansion in progress:
\secondoftwo{12%
\expandafter\secondoftwo % <- The interesting \secondoftwo
\expandafter{% <- Opening brace of interesting \secondoftwo's first argument.
\expandafter{%
\string{foo}bar}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is an opening brace (Scenario 1).
\expandafter
\secondoftwo\string}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is not an opening brace (Scenario 2).
\expandafter\firstoftwo\expandafter{\expandafter
\secondoftwo\string}%
\expandafter\stopromannumeral\secondoftwo}% <- Closing brace of interesting \secondoftwo's first argument in case #1 is empty (Scenario 3).
{\expandafter\stopromannumeral\firstoftwo}%
{empty}{not empty}%
Step 4: \secondoftwo removes {12:
%\romannumeral-expansion in progress:
\expandafter\secondoftwo % <- The interesting \secondoftwo
\expandafter{% <- Opening brace of interesting \secondoftwo's first argument.
\expandafter{%
\string{foo}bar}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is an opening brace (Scenario 1).
\expandafter
\secondoftwo\string}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is not an opening brace (Scenario 2).
\expandafter\firstoftwo\expandafter{\expandafter
\secondoftwo\string}%
\expandafter\stopromannumeral\secondoftwo}% <- Closing brace of interesting \secondoftwo's first argument in case #1 is empty (Scenario 3).
{\expandafter\stopromannumeral\firstoftwo}%
{empty}{not empty}%
Step 5: \expandafter-chain "hits" \string which in case of the argument not being empty strigifies the argument's first token and in case of the argument being empty stringifies the closing brace:
%\romannumeral-expansion in progress:
\secondoftwo % <- The interesting \secondoftwo
{% <- Opening brace of interesting \secondoftwo's first argument.
{%
{12foo}bar}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is an opening brace (Scenario 1).
\expandafter
\secondoftwo\string}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is not an opening brace (Scenario 2).
\expandafter\firstoftwo\expandafter{\expandafter
\secondoftwo\string}%
\expandafter\stopromannumeral\secondoftwo}% <- Closing brace of interesting \secondoftwo's first argument in case #1 is empty (Scenario 3).
{\expandafter\stopromannumeral\firstoftwo}%
{empty}{not empty}%
Step 6: The interesting \secondoftwo acts:
%\romannumeral-expansion in progress:
\expandafter
\secondoftwo\string}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is not an opening brace (Scenario 2).
\expandafter\firstoftwo\expandafter{\expandafter
\secondoftwo\string}%
\expandafter\stopromannumeral\secondoftwo}% <- Closing brace of interesting \secondoftwo's first argument in case #1 is empty (Scenario 3).
{\expandafter\stopromannumeral\firstoftwo}%
{empty}{not empty}%
Step 7: \expandafter "hits" \string and } gets stringified:
%\romannumeral-expansion in progress:
\secondoftwo}12% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is not an opening brace (Scenario 2).
\expandafter\firstoftwo\expandafter{\expandafter
\secondoftwo\string}%
\expandafter\stopromannumeral\secondoftwo}% <- Closing brace of interesting \secondoftwo's first argument in case #1 is empty (Scenario 3).
{\expandafter\stopromannumeral\firstoftwo}%
{empty}{not empty}%
Step 8: \secondoftwo removes }12:
%\romannumeral-expansion in progress:
\expandafter\firstoftwo\expandafter{\expandafter
\secondoftwo\string}%
\expandafter\stopromannumeral\secondoftwo}% <- Closing brace of interesting \secondoftwo's first argument in case #1 is empty (Scenario 3).
{\expandafter\stopromannumeral\firstoftwo}%
{empty}{not empty}%
Step 9: \expandafter-chain "hits" \string and } gets stringified:
%\romannumeral-expansion in progress:
\firstoftwo{\secondoftwo}12%
\expandafter\stopromannumeral\secondoftwo}% <- Closing brace of interesting \secondoftwo's first argument in case #1 is empty (Scenario 3).
{\expandafter\stopromannumeral\firstoftwo}%
{empty}{not empty}%
Step 10: \firstoftwo acts:
%\romannumeral-expansion in progress:
\secondoftwo}12%
\expandafter\stopromannumeral\secondoftwo
{empty}{not empty}%
Step 11: \secondoftwo removes }12:
%\romannumeral-expansion in progress:
\expandafter\stopromannumeral\secondoftwo
{empty}{not empty}%
Step 12: \expandafter "hits" \secondoftwo:
%\romannumeral-expansion in progress:
\stopromannumeral not empty%
Step 13: While still in the stage of expanding things in the course of gathering tokens that make up \romannumeral's ⟨number⟩-quantity TeX now encounters the token \stopromannumeral which denotes the non-positive number 0 in a way which stops TeX's gathering of tokens belonging to a ⟨number⟩-quantity. TeX removes the token forming the ⟨number⟩-quantity and - as that quantity's value is not positive - silently terminates the \romannumeral-process without delivering any token in return:
%\romannumeral-expansion terminated:
not empty%
Scenario 2: #1 is not empty and #1's first token is not an opening brace—e.g., #1=foobar:
\CheckWhetherEmpty{foobar}{empty}{not empty}%
Step 1: Toplevel-expansion of \CheckWhetherEmpty delivers the following tokens to TeX's gullet:
\romannumeral
\expandafter\secondoftwo\string{%
\expandafter\secondoftwo % <- The interesting \secondoftwo
\expandafter{% <- Opening brace of interesting \secondoftwo's first argument.
\expandafter{%
\string foobar}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is an opening brace (Scenario 1).
\expandafter
\secondoftwo\string}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is not an opening brace (Scenario 2).
\expandafter\firstoftwo\expandafter{\expandafter
\secondoftwo\string}%
\expandafter\stopromannumeral\secondoftwo}% <- Closing brace of interesting \secondoftwo's first argument in case #1 is empty (Scenario 3).
{\expandafter\stopromannumeral\firstoftwo}%
{empty}{not empty}%
Step 2: \romannumeral-expansion initiated:
%\romannumeral-expansion in progress:
\expandafter\secondoftwo\string{%
\expandafter\secondoftwo % <- The interesting \secondoftwo
\expandafter{% <- Opening brace of interesting \secondoftwo's first argument.
\expandafter{%
\string foobar}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is an opening brace (Scenario 1).
\expandafter
\secondoftwo\string}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is not an opening brace (Scenario 2).
\expandafter\firstoftwo\expandafter{\expandafter
\secondoftwo\string}%
\expandafter\stopromannumeral\secondoftwo}% <- Closing brace of interesting \secondoftwo's first argument in case #1 is empty (Scenario 3).
{\expandafter\stopromannumeral\firstoftwo}%
{empty}{not empty}%
Step 3: \expandafter "hits" \string and { gets stringified:
%\romannumeral-expansion in progress:
\secondoftwo{12%
\expandafter\secondoftwo % <- The interesting \secondoftwo
\expandafter{% <- Opening brace of interesting \secondoftwo's first argument.
\expandafter{%
\string foobar}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is an opening brace (Scenario 1).
\expandafter
\secondoftwo\string}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is not an opening brace (Scenario 2).
\expandafter\firstoftwo\expandafter{\expandafter
\secondoftwo\string}%
\expandafter\stopromannumeral\secondoftwo}% <- Closing brace of interesting \secondoftwo's first argument in case #1 is empty (Scenario 3).
{\expandafter\stopromannumeral\firstoftwo}%
{empty}{not empty}%
Step 4: \secondoftwo removes {12:
%\romannumeral-expansion in progress:
\expandafter\secondoftwo % <- The interesting \secondoftwo
\expandafter{% <- Opening brace of interesting \secondoftwo's first argument.
\expandafter{%
\string foobar}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is an opening brace (Scenario 1).
\expandafter
\secondoftwo\string}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is not an opening brace (Scenario 2).
\expandafter\firstoftwo\expandafter{\expandafter
\secondoftwo\string}%
\expandafter\stopromannumeral\secondoftwo}% <- Closing brace of interesting \secondoftwo's first argument in case #1 is empty (Scenario 3).
{\expandafter\stopromannumeral\firstoftwo}%
{empty}{not empty}%
Step 5: \expandafter-chain "hits" \string which in case of the argument not being empty strigifies the argument's first token and in case of the argument being empty stringifies the closing brace:
%\romannumeral-expansion in progress:
\secondoftwo % <- The interesting \secondoftwo
{% <- Opening brace of interesting \secondoftwo's first argument.
{%
f12oobar}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is an opening brace (Scenario 1).
\expandafter
\secondoftwo\string}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is not an opening brace (Scenario 2).
\expandafter\firstoftwo\expandafter{\expandafter
\secondoftwo\string}%
\expandafter\stopromannumeral\secondoftwo}% <- Closing brace of interesting \secondoftwo's first argument in case #1 is empty (Scenario 3).
{\expandafter\stopromannumeral\firstoftwo}%
{empty}{not empty}%
Step 6: The interesting \secondoftwo acts:
%\romannumeral-expansion in progress:
\expandafter\firstoftwo\expandafter{\expandafter
\secondoftwo\string}%
\expandafter\stopromannumeral\secondoftwo}% <- Closing brace of interesting \secondoftwo's first argument in case #1 is empty (Scenario 3).
{\expandafter\stopromannumeral\firstoftwo}%
{empty}{not empty}%
Step 7: \expandafter-chain "hits" \string and } gets stringified:
%\romannumeral-expansion in progress:
\firstoftwo{\secondoftwo}12%
\expandafter\stopromannumeral\secondoftwo}% <- Closing brace of interesting \secondoftwo's first argument in case #1 is empty (Scenario 3).
{\expandafter\stopromannumeral\firstoftwo}%
{empty}{not empty}%
Step 8: \firstoftwo acts:
%\romannumeral-expansion in progress:
\secondoftwo}12%
\expandafter\stopromannumeral\secondoftwo
{empty}{not empty}%
Step 9: \secondoftwo removes }12:
%\romannumeral-expansion in progress:
\expandafter\stopromannumeral\secondoftwo
{empty}{not empty}%
Step 10: \expandafter "hits" \secondoftwo:
%\romannumeral-expansion in progress:
\stopromannumeral not empty%
Step 11: While still in the stage of expanding things in the course of gathering tokens that make up \romannumeral's ⟨number⟩-quantity TeX now encounters the token \stopromannumeral which denotes the non-positive number 0 in a way which stops TeX's gathering of tokens belonging to a ⟨number⟩-quantity. TeX removes the token forming the ⟨number⟩-quantity and - as that quantity's value is not positive - silently terminates the \romannumeral-process without delivering any token in return:
%\romannumeral-expansion terminated:
not empty%
Scenario 3: #1 is empty:
\CheckWhetherEmpty{}{empty}{not empty}%
Step 1: Toplevel-expansion of \CheckWhetherEmpty delivers the following tokens to TeX's gullet:
\romannumeral
\expandafter\secondoftwo\string{%
\expandafter\secondoftwo % <- The interesting \secondoftwo
\expandafter{% <- Opening brace of interesting \secondoftwo's first argument.
\expandafter{%
\string}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is an opening brace (Scenario 1).
\expandafter
\secondoftwo\string}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is not an opening brace (Scenario 2).
\expandafter\firstoftwo\expandafter{\expandafter
\secondoftwo\string}%
\expandafter\stopromannumeral\secondoftwo}% <- Closing brace of interesting \secondoftwo's first argument in case #1 is empty (Scenario 3).
{\expandafter\stopromannumeral\firstoftwo}%
{empty}{not empty}%
Step 2: \romannumeral-expansion initiated:
%\romannumeral-expansion in progress:
\expandafter\secondoftwo\string{%
\expandafter\secondoftwo % <- The interesting \secondoftwo
\expandafter{% <- Opening brace of interesting \secondoftwo's first argument.
\expandafter{%
\string}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is an opening brace (Scenario 1).
\expandafter
\secondoftwo\string}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is not an opening brace (Scenario 2).
\expandafter\firstoftwo\expandafter{\expandafter
\secondoftwo\string}%
\expandafter\stopromannumeral\secondoftwo}% <- Closing brace of interesting \secondoftwo's first argument in case #1 is empty (Scenario 3).
{\expandafter\stopromannumeral\firstoftwo}%
{empty}{not empty}%
Step 3: \expandafter "hits" \string and { gets stringified:
%\romannumeral-expansion in progress:
\secondoftwo{12%
\expandafter\secondoftwo % <- The interesting \secondoftwo
\expandafter{% <- Opening brace of interesting \secondoftwo's first argument.
\expandafter{%
\string}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is an opening brace (Scenario 1).
\expandafter
\secondoftwo\string}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is not an opening brace (Scenario 2).
\expandafter\firstoftwo\expandafter{\expandafter
\secondoftwo\string}%
\expandafter\stopromannumeral\secondoftwo}% <- Closing brace of interesting \secondoftwo's first argument in case #1 is empty (Scenario 3).
{\expandafter\stopromannumeral\firstoftwo}%
{empty}{not empty}%
Step 4: \secondoftwo removes {12:
%\romannumeral-expansion in progress:
\expandafter\secondoftwo % <- The interesting \secondoftwo
\expandafter{% <- Opening brace of interesting \secondoftwo's first argument.
\expandafter{%
\string}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is an opening brace (Scenario 1).
\expandafter
\secondoftwo\string}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is not an opening brace (Scenario 2).
\expandafter\firstoftwo\expandafter{\expandafter
\secondoftwo\string}%
\expandafter\stopromannumeral\secondoftwo}% <- Closing brace of interesting \secondoftwo's first argument in case #1 is empty (Scenario 3).
{\expandafter\stopromannumeral\firstoftwo}%
{empty}{not empty}%
Step 5: \expandafter-chain "hits" \string which in case of the argument not being empty strigifies the argument's first token and in case of the argument being empty stringifies the closing brace:
%\romannumeral-expansion in progress:
\secondoftwo % <- The interesting \secondoftwo
{% <- Opening brace of interesting \secondoftwo's first argument.
{%
}12% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is an opening brace (Scenario 1) got stringified.
\expandafter
\secondoftwo\string}% <- Closing brace of interesting \secondoftwo's first argument in case #1's first token is not an opening brace (Scenario 2).
\expandafter\firstoftwo\expandafter{\expandafter
\secondoftwo\string}%
\expandafter\stopromannumeral\secondoftwo}% <- Closing brace of interesting \secondoftwo's first argument in case #1 is empty (Scenario 3).
{\expandafter\stopromannumeral\firstoftwo}%
{empty}{not empty}%
Step 6: The interesting \secondoftwo acts:
%\romannumeral-expansion in progress:
\expandafter\stopromannumeral\firstoftwo
{empty}{not empty}%
Step 7: \expandafter "hits" \firstoftwo:
%\romannumeral-expansion in progress:
\stopromannumeral empty%
Step 8: While still in the stage of expanding things in the course of gathering tokens that make up \romannumeral's ⟨number⟩-quantity TeX now encounters the token \stopromannumeral which denotes the non-positive number 0 in a way which stops TeX's gathering of tokens belonging to a ⟨number⟩-quantity. TeX removes the token forming the ⟨number⟩-quantity and - as that quantity's value is not positive - silently terminates the \romannumeral-process without delivering any token in return:
%\romannumeral-expansion terminated:
empty%
Based on that you can implement an \ifblank-test as follows:
%%-----------------------------------------------------------------------------
%% Check whether argument is blank (empty or only spaces):
%%-----------------------------------------------------------------------------
%% -- Take advantage of the fact that TeX discards space tokens when
%% "fetching" _un_delimited arguments: --
%% \CheckWhetherBlank{<Argument which is to be checked>}%
%% {<Tokens to be delivered in case that
%% argument which is to be checked is blank>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked is not blank}%
\long\def\CheckWhetherBlank#1{%
\romannumeral\expandafter\expandafter\expandafter\secondoftwo
\expandafter\CheckWhetherEmpty\expandafter{\firstoftwo#1{}.}%
}%
The dot will be the second argument of \firstoftwo only if #1 is blank.
Thus the dot will be removed only in case #1 is blank.
Thus only in case #1 is blank the argument of \CheckWhetherEmpty is empty.
Based on the gist of the implementation of \CheckWhetherEmpty you can implement checking whether a non-delimited argument's first token is an explicit character token of category code 1 (begin group): Just ensure by appending a dot that the \string which gets carried out right before executing the "interesting \secondoftwo" never "hits" a closing brace (which implies elimination of scenario 3) and implement forking between scenario 1 and scenario 2:
%%-----------------------------------------------------------------------------
%% Check whether argument's first token is a catcode-1-character
%%-----------------------------------------------------------------------------
%% \CheckWhetherBrace{<Argument which is to be checked>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked has leading
%% catcode-1-token>}%
%% {<Tokens to be delivered in case that argument
%% which is to be checked has no leading
%% catcode-1-token>}%
%%
%% Due to \romannumeral0-expansion the result is delivered after two
%% expansion-steps/after two "hits" by \expandafter.
%%
\long\def\CheckWhetherBrace#1{%
\romannumeral\expandafter\secondoftwo\expandafter{\expandafter{%
\string#1.}\expandafter\firstoftwo\expandafter{\expandafter
\secondoftwo\string}\expandafter\stopromannumeral\firstoftwo}%
{\expandafter\stopromannumeral\secondoftwo}%
}%
\detokenizel3 is older than etex.... – David Carlisle Oct 23 '19 at 10:06\if aa\fiversus\ifx aa\fiand the latter is slightly faster, but\expandafter\ifx aa\fiis noticebly slower. However,\if aa\fiis noticeably slower than\ifx\a\a\fi(with\def\a{a}) and\expandafter\ifx\aa\fiperforms just slightly slower than\if aa\fi. – egreg Oct 23 '19 at 13:47\ifxand\ifare more or less equally fast (at least for single tokens, which is the case in the question), and what slows the process down is the\expandafter... – Phelype Oleinik Oct 23 '19 at 14:24\detokenizeversions performs compared to other emptiness tests when it comes to the length of the argument. As far as I understand,\detokenizehas to go through the whole list before the comparison.even starts. So for very long lists it should be notably slower than e.g. a naive\ifx\relax#1\relax. – siracusa Oct 23 '19 at 14:31\detokenizeslows down the operation by a considerable amount. However for what I'm trying to do I need the\detokenizeapproach because it has to cope with possibly unbalanced conditionals in the argument, in which case other approaches all fail. Thanks for pointing it out! – Phelype Oleinik Oct 23 '19 at 14:36\prg_new_conditional:Npnn, but instead code the test yourself, because the way\prg_new_conditional:Npnnsets up the branching is slow (in the produced code, not during the definition, hint: it uses\expandafter). Instead, if you want the last tiny bit of performance you should use\cs_new:Npn \__pho_fi_use_i:wnn \fi: \use_ii:nn #1 #2 { \fi: #1 } \cs_new:Npn \pho_tl_if_empty:nTF #1 { \if:w \scan_stop: \tl_to_str:n { #1 } \scan_stop: \__pho_fi_use_i:wnn \fi: \use_ii:nn }– Skillmon Nov 13 '19 at 18:42TorFbranches anymore (although if I needed I could define them manually). Depending when, it may be worth it. Although my actual use case (never trust OP :-) is\if_catcode:w \scan_stop: \tl_to_str:n \exp_after:wN { \use_none:n #1 } \scan_stop: ^ \fi:in a three-way sort-of conditional. Anyway, thanks for the suggestion! – Phelype Oleinik Nov 13 '19 at 19:49TandFversion is easy, too, use\cs_new:Npn \__pho_fi_use_i:wn \fi: \use_none:n #1 { \fi: #1 }and\cs_new:Npn \__pho_fi_use_none:wn \fi: \use_i:n #1 { \fi: }but you have to put some duplicate code there (the actual\if...test). Still the general concept can be applied, if you know what the read tokens will be, it is faster to define the macro with a delimited argument instead of gobbling the token as an actual argument. And in general\expandafteris slow, so if you can get around it with the same number of expansions, do it. – Skillmon Nov 14 '19 at 01:28TandFvariants was only the tradeoff between four hundredths of a second and the code duplication. You said "it is faster to define the macro with a delimited argument instead of gobbling the token as an actual argument": do you have any reference or that's just from overusingl3benchmark? – Phelype Oleinik Nov 14 '19 at 01:35\def\foo#1\qstopbe faster than\def\foo\a\b\c\d\e\f\g\h\i\j\k\l\m\n\o\p\q\r\s\t\u\v\w\x\y\z\qstop?). Another thing that is generally faster is, don't read in an argument twice if you can read it once, better to add another token (e.g., see https://tex.stackexchange.com/a/515744/117050 the non-expandable version and the macro...@false, but this way it gets vulnerable against input like1pt, instead of1pt). – Skillmon Nov 14 '19 at 01:53l3benchmarkat it, it scales pretty well, the latter being considerably faster than the former, taking only about 63% of the time. – Skillmon Nov 14 '19 at 02:03tex.pdfand found the relevant bits in §291 (general description of how TeX stores token lists) and §397 (specific procedure to matches a non-parameter parameter text). Using an argument is slow because it a) triggers a procedure to scan a parameter (delimited by whatever, which also needs checking and specific procedures), b) stores the scanned parameter in apstackarray, and c) retrieves eachout_paramin the replacement text from thepstack. Explicit delimiters are just matched and discarded. – Phelype Oleinik Nov 14 '19 at 03:17\foo\a...\z(imagine...being the missing letters :-) would be to define it with\def\foo#1\a...\z{}and then use with\foo\a...\y\a...\zso that the scanner in §397 is thrown off the track in the first try. But in that case the programmer would be asking for it ;-) – Phelype Oleinik Nov 14 '19 at 03:28\ifemptytest that only fails if the argument contains\ifempty@A\ifempty@Bdirectly after each other (taking about 70% the time a\if\relax\detokenize{#1}\relaxtakes):\long\def\ifempty@true\ifempty@A\ifempty@B\@secondoftwo#1#2{#1}\long\def\ifempty@#1\ifempty@A\ifempty@B{}\long\def\ifempty#1{\ifempty@\ifempty@A#1\ifempty@B\ifempty@true\ifempty@A\ifempty@B\@secondoftwo}– Skillmon Nov 19 '19 at 09:21\if\relax\detokenizetest in favour of yours now :-) – Phelype Oleinik Nov 19 '19 at 19:39\if\relax\detokenize{<token-list>}\relax...I might probably do\ifcat$\detokenize{<token-list>}$...in order to not have to rely on\relaxnot being redefined in a way which fools the test. – Ulrich Diez Jul 04 '21 at 01:23