Refactor to remove goto
Yes, of course. (It's always possible to eliminate unstructured constructs by use of structured programming.) I recommend Kernighan &amp; Plauger's The Elements of Programming Style for developing your own sense of structured programming.

Come on this journey with me!

Step 1: That confusing if at the top of the loop.

while (true) {
    const char c1 = *i;
    if( c1 != '*' )
    {
        i++;
        if( i != fileEnd )
            continue;
        break;
    }


c1 is never used again, so we can eliminate it. Then, breaking the loop means going to the end of the loop and setting a "don't do this loop again" flag; so let's make that flag; we'll call it done. Then continueing the loop means going to the end without setting that flag. So at this point we have

template&lt;class It&gt;
It skipMultilineComment(It it, It fileEnd)
{
    bool done = false;
    while (!done) {
        if (*it != '*') {
            ++it;
            done = (it == fileEnd);
        } else {
            // Found '*' character
        FoundAsterisk:
            ++it;
            if (it == fileEnd) break;
            if (*it == '*') {
                /* Found another '*' character; can be more: ****/
                goto FoundAsterisk;
            } else if (*it != '/') {
                ++it;
                if (it != fileEnd) continue;
                break;
            }
            // Finally, found the closing token, "*/"
            ++it;
            return it;
        }
    }
    logError( "fatal error C1071: unexpected end of file found in comment" );
    throw E_INVALIDARG;
}


Notice that I've cleaned up some whitespace style and renamed i (traditionally a name for an integer loop control variable) to it (traditionally a name for an iterator). I've also made the function a template so that I can get rid of that global-scope typedef at the top.

Okay, let's pull out the next break... or, no, let's skip down to that similar continue/break tangle at the bottom of the loop, and sort that out. Same transformation as before. I'll just show the loop, because nothing outside it has changed.

    while (!done) {
        if (*it != '*') {
            ++it;
            done = (it == fileEnd);
        } else {
            // Found '*' character
        FoundAsterisk:
            ++it;
            if (it == fileEnd) break;
            if (*it == '*') {
                /* Found another '*' character; can be more: ****/
                goto FoundAsterisk;
            } else if (*it == '/') {
                // Finally found the closing token, "*/"
                ++it;
                return it;
            } else {
                ++it;
                done = (it == fileEnd);
            }
        }
    }


Notice that I am also habitually untangling your if/else blocks. You don't want to have if (a == x) ... else if (a != y) ... else if (a == z) ... because that's just plain confusing. if/else chains should read like switch statements: one handler per interesting value. So here we have one handler for *it == '*', and one handler for *it == '/', and then one catch-all "else" handler.

Let's follow that guideline and refactor the outer if (*it != '*') as well.
Notice that once we do that, we don't need the comment // Found '*' character anymore, because it's obvious from the code itself. Getting to remove pointless comments is one of the most satisfying parts of the refactoring process!

    while (!done) {
        if (*it == '*') {
        FoundAsterisk:
            ++it;
            if (it == fileEnd) {
                done = true;
            } else if (*it == '*') {
                goto FoundAsterisk;
            } else if (*it == '/') {
                // Finally found the closing token, "*/"
                ++it;
                return it;
            } else {
                ++it;
                done = (it == fileEnd);
            }
        } else {
            ++it;
            done = (it == fileEnd);
        }
    }


Okay, let's tackle that goto. The fundamental algorithm here is, "While we're looking at a * character, increment it. But if we reach the end of the string, then stop." Normally we'd spell that as

while (it != fileEnd &amp;&amp; *it == '*') { ++it; }


Let's see if we can shoehorn that line of code into our function in a natural way.

    while (!done) {
        if (*it == '*') {
            while (it != fileEnd &amp;&amp; *it == '*') {
                ++it;
            }
            if (it == fileEnd) {
                done = true;
            } else if (*it == '/') {
                // Finally found the closing token, "*/"
                ++it;
                return it;
            } else {
                ++it;
                done = (it == fileEnd);
            }
        } else {
            ++it;
            done = (it == fileEnd);
        }
    }


Notice that every path to the bottom of the outer loop now ends with done = (it == fileEnd) (except in one case where we already know it == fileEnd and so we just set done = true). So basically we just keep going until it == fileEnd. That's our loop condition.

    while (it != fileEnd) {
        if (*it == '*') {
            while (it != fileEnd &amp;&amp; *it == '*') {
                ++it;
            }
            if (it == fileEnd) {
            } else if (*it == '/') {
                // Finally found the closing token, "*/"
                ++it;
                return it;
            } else {
                ++it;
            }
        } else {
            ++it;
        }
    }


Okay, but, now we've got a loop within a loop. You know what? Maybe what we really want here is a simple state machine: Either we've just seen a *, or we haven't. If we have, and the next character is /, then we're done. Otherwise, keep going. That would code up like this:

template&lt;class It&gt;
It skipMultilineComment(It it, It fileEnd)
{
    for (bool seenStar = false; it != fileEnd; ++it) {
        if (*it == '/' &amp;&amp; seenStar) {
            return it + 1;
        }
        seenStar = (*it == '*');
    }
    logError( "fatal error C1071: unexpected end of file found in comment" );
    throw E_INVALIDARG;
}


Yes. I like that better. But wait, we can do better than that! All we seem to be doing is looking for the string "*/" inside a longer string. That's string search, and there are library functions for that!

#include &lt;string.h&gt;

const char *skipMultilineComment(const char *it, const char *fileEnd)
{
    if (const char *p = memmem(it, fileEnd - it, "*/", 2)) {
        return p + 2;
    }
    logError( "fatal error C1071: unexpected end of file found in comment" );
    throw E_INVALIDARG;
}


or in C++17,

std::string_view skipMultilineComment(std::string_view text)
{
    size_t pos = text.find("*/");
    if (pos != text.npos) {
        return text.substr(pos + 2);
    }
    logError( "fatal error C1071: unexpected end of file found in comment" );
    throw E_INVALIDARG;
}