ptrace’s manpage is a little confusing about the effect of the PTRACE_O_TRACEEXEC option:

First we have,

If the PTRACE_O_TRACEEXEC option is not in effect, all successful
calls to execve(2) by the traced process will cause it to be sent
a SIGTRAP signal, giving the parent a chance to gain control
before the new program begins execution.

and then, further down,

PTRACE_O_TRACEEXEC (since Linux 2.5.46)
        Stop the tracee at the next execve(2).  A
        waitpid(2) by the tracer will return a status value
        such that

        status>>8 == (SIGTRAP | (PTRACE_EVENT_EXEC<<8))

        If the execing thread is not a thread group leader,
        the thread ID is reset to thread group leader's ID
        before this stop.  Since Linux 3.0, the former
        thread ID can be retrieved with PTRACE_GETEVENTMSG.

Whether we have PTRACE_O_TRACEEXEC or not, the tracee/child will be stopped at its next execve(). The difference lies in what status does the tracer/parent gets from wait()/waitpid().

If PTRACE_O_TRACEEXEC, the kernel will make sure that status>>8 == (SIGTRAP | (PTRACE_EVENT_EXEC<<8)) (as the document mentions) for the wait()/waitpid() corresponding to the execve().

If NO PTRACE_O_TRACEEXEC, the kernel will send a SIGTRAP (also as the document mentions) to the tracee/child, which will not be distinguishable with other SIGTRAPs, such as if the tracee/child raise(SIGTRAP).

Consider the following (not actually compiling) program:

void parent_tracer() {
    ptrace(PTRACE_SETOPTIONS, child, NULL, PTRACE_O_TRACEEXEC);

    int status;
    wait(&status);

    if (status == SIGTRAP)
        printf("child raise()");
    else {
        assert(status>>8 == (SIGTRAP | (PTRACE_EVENT_EXEC<<8)));
        printf("child execve()");
    }
}

void child_tracee() {
    if (rand() % 2)
        execve(...);
    else
        raise(SIGTRAP);
}