If you run this under strace, you can see that the version that uses ls starts up the command in a subshell, where the version which uses echo executes it all in the existing shell.
Compare the output of
$ strace -f /bin/bash -o trace.txt -c 'i=5; echo $i; echo file_c-$((++i)).txt; echo $i' 5 6 6
against
strace -f /bin/bash -o trace.txt -c 'i=5; echo $i; ls > file_c-$((++i)).txt; echo $i' 5 5
You'll see in the first:
1251 execve("/bin/bash", ["/bin/bash", "-c", "i=5; echo $i; echo file_c-$(( ++"...], [/* 19 vars */]) = 0 ... 1251 write(1, "5\n", 2) = 2 1251 write(1, "file_c-6.txt\n", 13) = 13 1251 write(1, "6\n", 2) = 2
And in the second:
1258 execve("/bin/bash", ["/bin/bash", "-c", "i=5; echo $i; ls > file_c-$(( ++"...], [/* 19 vars */]) = 0 ... 1258 write(1, "5\n", 2) = 2 ... 1258 stat("/bin/ls", {st_mode=S_IFREG|0755, st_size=110080, ...}) = 0 1258 access("/bin/ls", R_OK) = 0 1258 clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7301f40a10) = 1259 1259 open("file_c-6.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3 1259 dup2(3, 1) = 1 1259 close(3) = 0 1259 execve("/bin/ls", ["ls"], [/* 19 vars */]) = 0 1259 write(1, "71\nbin\nfile_a-5.txt\nfile_b-5.txt"..., 110) = 110 1259 close(1) = 0 1259 munmap(0x7f0e81c56000, 4096) = 0 1259 close(2) = 0 1259 exit_group(0) = ? 1259 +++ exited with 0 +++ 1258 <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 1259 1258 rt_sigaction(SIGINT, {SIG_DFL, [], SA_RESTORER, 0x7f7301570d40}, {0x4438a0, [], SA_RESTORER, 0x7f7301570d40}, 8) = 0 1258 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0 1258 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=1259, si_status=0, si_utime=0, si_stime=0} --- 1258 wait4(-1, 0x7ffd23d86e98, WNOHANG, NULL) = -1 ECHILD (No child processes) 1258 rt_sigreturn() = 0 1258 write(1, "5\n", 2) = 2
In this last example, you see the clone into a new process (from 1258 -> 1259), so now we're in a subprocess. The opening of file_c-6.txt which means that we've evaluated $((++i)) in the subshell, and the execution of ls with its stdout set to that file.
Finally, we see that the subprocess exits, we reap the child, then we continue with where we left off... with $i set to 5, and that's what we echo out again.
(Remember variable changes in a subprocess do not percolate up to the parent process, unless you do something explicitly in the parent to grab the child's changes)
echoinstead ofls, it works the second way in both the cases./bin/echopreserves the difference, so it seems like output redirections for external commands happen in a subshell.