Writing a state machine such that state transitions map to instruction pointer changes can be a viable technique. For example, using Pascal - handy because of sets - your example could be recognized thusly, with cp: PChar, or ^Char:
start := cp;
if cp^ in ['+', '-'] then Inc(cp);
if cp^ in ['1'..'9'] then Inc(cp) else goto no_match;
while cp^ in ['0'..'9'] do Inc(cp);
SetString(str, start, cp - start);
I don't think this is tedious at all. The various components of a regular expression have analogues in code: * and + become loops, ? and | if statements, character classes become set membership tests (which is nice in Pascal). For most token definitions used in practical programming languages, writing lexers for them by hand is fairly trivial; and when things get more complicated, the freedom of having hand-coded the lexer usually gives you more tools to solve those complications.
Having a lexer coded as a loop driven by state transition tables can be useful in other scenarios, however; for example, if you only receive a little bit of input at a time, and you want to continuously lex it until a whole token has formed. There, you want to be able to suspend tokenizing at any point, and resume it later. Without coroutines, continuations or threads, this is more awkward when hand-coding.
Having a lexer coded as a loop driven by state transition tables can be useful in other scenarios, however; for example, if you only receive a little bit of input at a time, and you want to continuously lex it until a whole token has formed. There, you want to be able to suspend tokenizing at any point, and resume it later. Without coroutines, continuations or threads, this is more awkward when hand-coding.