Giving a process the right to change its UID is equivalent to giving it the right to run as root, since it can just change its UID to root. So even if you did that, there'd be no benefit.
You can improve the security of this setup by reducing the risk that the program can be tricked into doing something unintended as root. This won't reduce the impact of a complete compromise (if an attacker is able to make your program run arbitrary code), but it can reduce the impact of a partial compromise (where the attacker is only able to convince your program to do some specific thing, e.g. to read a file and display it). To reduce the risk, reduce the window of time during which the program runs as root. Make it run as a dedicated user, but keep its real UID set to 0. When the program needs to be root, it temporarily switches its effective UID to 0, and then switches back.
Note that this is not really practical in a multithreaded program since the user ID is for a whole process, not per thread.
For better robustness and security, implement privilege separation: split your program into two processes, one that runs as root and one that runs as a dedicated user. The process that runs as root should do as little as possible. It takes requests from the other process, validates them to the extent possible, and performs them. Then, even if the other process is buggy, the most it can do is issue valid requests.
For simple needs, rather than write a program that runs as root, you may be able to have a sudo rule that directly allows the main program to run commands as a third user. Instead of relying on an intermediate root program to perform validation, you can embed the validation in the sudo rule, e.g.
myapp_user ALL = (ALL:ALL) validate_and_run
myapp_user to run a command as any user, but the command has to be
validate_and_run (with any arguments).