How to return another post than requested

If you can access the ID of your post, it means that the query has already been run. So, in this case, you can’t use pre_get_posts anymore.

A solution would be to wrap your code in a conditional, and then use functions such as get_the_permalink() or get_the_title() which accept a post ID, to get the content for another post:

if( get_the_ID() == '5' ){
    get_the_title( 10 );
} else {
    // Main loop
}

The above should be used in your templates.

Another reasonable and better approach would be to use redirection. Check the post’s ID, and then redirect the user if the conditional matches:

if( get_the_ID() == '5' ) {
    wp_safe_redirect( get_the_permalink( 10 ) );
    exit();
}

This can be used either in functions.php or the header of your templates.

There’s also a hack you might want to use, to get pre_get_posts to work.

add_action( 'pre_get_posts', 'my_function' );
function my_function( $query ) {
    // Remove this action hook so we don't go into an infinite loop
    remove_action( 'pre_get_posts', 'my_function' );
    // Get a list of posts belonging to this query
    $posts = get_posts( $query->query );
    // If there is only one post in the array and its ID matches
    // what we're looking for, then we can alter the main query
    if( $query->is_main_query() && sizeOf( $posts ) == '1' && $post[0]->ID == '5' ) {
        // Now we can modify the query here
    }
}