2
#include <iostream>
#include <cstdio>

using namespace std;

int main() {
    auto file = freopen("./input.txt", "r", stdin);

    if (file == nullptr) {
        cout << "freopen failed" << '\n';
    } else {
        int n;
        cin >> n;
        cout << n << endl;
    }

    return 0;
}

This program reads an integer in a file and outputs it. It runs as intended on any other shells, such as cmd, powershell and Msys2 bash. But on git bash, it always outputs 0 no matter the input file. It also fails when freopen is used for stdout.

I compiled with g++.exe (Rev10, Built by MSYS2 project) 12.2.0 downloaded with MSYS2.

Actually, I did find the solution to fix the problem. I copied libstdc++-6.dll in MSYS's mingw64 (in msys2/ucrt64/bin to be exact) and overwrote libstdc++-6.dll in git's mingw64 and it started to work. It seems like when the program is executed on git bash, it links git's mingw64's dll first and it caused freopen to not work.

However, I still want to know why this happened in the first place, and whether my solution possibly can cause a problem in the future.

  1. Is git-for-windows' libstdc++ bugged? If not, what was the cause of the problem? Is git bash only for using git, not intended for running executables and such?
  2. Is there any chance my solution will cause another problem? If so, is there any other better solution?
Cho98
  • 21
  • 2
  • 2
    I highly recommend you invest in [some good C++ books](https://stackoverflow.com/questions/388242/the-definitive-c-book-guide-and-list) to learn C++ properly, including using files the C++ way. Using `freopen` like you do is a rather dirty trick taught by some sites with bad reputation. – Some programmer dude Mar 16 '23 at 11:49
  • @Someprogrammerdude I get that `freopen` is not the best way to do this kind of task, but I was using it for a short competitive-programming (or problem-solving) code. – Cho98 Mar 16 '23 at 11:55
  • 2
    IIRC `cin` and `stdin` are not guaranteed to be connected to each other. Reopening `stdin` might not affect `cin`. – user253751 Mar 16 '23 at 12:00
  • Like I said, commonly taught and used on sites with bad reputation. :/ – Some programmer dude Mar 16 '23 at 12:14
  • Did you try with [Git 2.40](https://github.com/git-for-windows/git/releases)? – VonC Mar 16 '23 at 12:37
  • @VonC Yes! My git version is 2.40. – Cho98 Mar 16 '23 at 12:49
  • @user253751 After changing `cin` and `cout` to `scanf` and `printf`, the program runs as intended. Apparently, with git's libstdc, there are some problems with `freopen` redirecting `cin`. However, I doubt it's an undefined behavior, since from what I've searched [here](https://cplusplus.github.io/LWG/issue49), redirecting stdio should redirec iostream too? – Cho98 Mar 16 '23 at 13:02
  • @Cho98 that's a proposal. They're saying it *should*. – user253751 Mar 16 '23 at 13:03
  • @Cho98 [`msys2/MSYS2-packages` issue 682](https://github.com/msys2/MSYS2-packages/issues/682) uses `--replace=freopen=_freopen64` would that work better? – VonC Mar 16 '23 at 13:04
  • 1
    @user253751 Sorry I posted a proposal. Nonetheless, it seems like it was proposed over 2 decades ago and the [reference](https://en.cppreference.com/w/cpp/io/ios_base/sync_with_stdio) and [current draft](https://eel.is/c++draft/ios.members.static) and previous drafts also state the same. – Cho98 Mar 16 '23 at 13:24
  • 1
    @user253751 I think the default value is true. The reference says "By default, all eight standard C++ streams are synchronized with their respective C streams" – Cho98 Mar 16 '23 at 13:28
  • @VonC I don't think c++ has `freopen64` or `_freopen64` function. – Cho98 Mar 16 '23 at 13:35
  • not sure about the standard, but you can actually just do `std::ofstream cin("filename");` to create a local `cin` and ignore all the `C` things. (note: not saying it's a good idea to do it.) – apple apple Mar 16 '23 at 16:08

1 Answers1

2

The problem is that stdin and cin are buffered I/O wrappers around the low-level operating system routines, and are separate from one another. (That's also why you shouldn't mix them for input, because one could buffer parts of a read.)

freopen() will replace the stdin wrapper, but the underlying operating system file descriptor and/or handle will not necessarily be replaced (and on your system it won't), so cin will still be connected to the underlying handle.

To my knowledge there is no portable way of changing cin in C++.

However, if you only want to support the most common operating systems, there are possibilities:

POSIX: On POSIX-based operating systems, such as Linux, macOS, FreeBSD, etc., you can use the low-level open() function (#include <fcntl.h>) to open the file and then use dup2() to replace the file descriptor 0, which is the standard input file descriptor. You'd want to use the following code:

#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

#include <stdexcept>
// for std::format, C++20 and higher
#include <format>

int fd = open(fileName, O_RDONLY);
if (fd < 0) {
    int error = errno;
    throw std::runtime_error(std::format("Couldn't open file: {}", strerror(error)));
}
int r = dup2(fd, 0);
if (r < 0) {
    int error = errno;
    close(fd);
    throw std::runtime_error(std::format("Couldn't replace input file descriptor: {}", strerror(error)));
}
// Close the original opened descriptor
close(fd);

(Technical note for experts: if your OS supports it you might also want to pass O_CLOEXEC to the flags of open(), but please make sure that dup2() also clears that flag on your OS, which it does on Linux, because the standard descriptors should not have that flag set. This implementation has a tiny race condition due to not supplying that flag, but since we immediately close fd after the call to dup2(), I'm personally ok with that, you typically don't replace your standard input descriptor in a multi-threaded environment anyway.)

Windows: On Windows it's a bit more complicated. You can use the SetStdHandle() Win32 API function to replace the input file handle itself, but that doesn't help with the C runtime (which also underlies the C++ runtime in some cases), because that has a copy of the underlying handle. So you need to also use _open_osfhandle() to get a file descriptor from the handle, and use _dup2() to dup it (similar to the POSIX version, but the file descriptors here are just for the C runtime itself).

The following code should work on Windows:

#include <windows.h>
#include <fcntl.h>

#include <stdexcept>
// for std::format, C++20 and higher
#include <format>

// This is so that the handle can be inherited by
// child processes.
SECURITY_ATTRIBUTES attrs{};
attrs.nLength = sizeof(attrs);
attrs.lpSecurityDescriptor = nullptr;
attrs.bInheritHandle = TRUE;
// use CreateFileW if fileName is a Unicode wide string
HANDLE handle = CreateFileA(fileName, GENERIC_READ, FILE_SHARE_READ, &attrs, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (handle == INVALID_HANDLE_VALUE) {
    DWORD error = GetLastError();
    throw std::runtime_error(std::format("Couldn't open file: error code {:d}", error));
}
BOOL ok = SetStdHandle(STD_INPUT_HANDLE, handle);
if (!ok) {
    DWORD error = GetLastError();
    CloseHandle(handle);
    throw std::runtime_error(std::format("Couldn't use handle as standard input: error code {:d}", error));
}
/* _open_osfhandle will take ownership of the handle itself, so we
 * have to provide it with a duplicate, because we call _close()
 * at the bottom, and we don't want the handle we passed to
 * SetStdHandle() to be closed, because that causes some weird
 * failures in some weird places
 */
HANDLE handleForDescriptor{INVALID_HANDLE_VALUE};
ok = DuplicateHandle(GetCurrentProcess(), handle, GetCurrentProcess(), &handleForDescriptor, 0, FALSE, DUPLICATE_SAME_ACCESS);
if (!ok) {
    DWORD error = GetLastError();
    /* Don't CloseHandle() here, because we already used
     * SetStdHandle, so the handle is already part of the
     * global state.
     */
    throw std::runtime_error(std::format("Couldn't duplicate handle: error code {:d}", error));
}
int fd = _open_osfhandle(reinterpret_cast<intptr_t>(handleForDescriptor), _O_RDONLY | _O_TEXT);
if (fd < 0) {
    DWORD error = GetLastError();
    CloseHandle(handleForDescriptor);
    throw std::runtime_error(std::format("Couldn't get file descriptor for handle: error code {:d}", error));
}
int r = _dup2(fd, 0);
if (r < 0) {
    DWORD error = GetLastError();
    _close(fd);
    throw std::runtime_error(std::format("Couldn't get file descriptor for handle: error code {:d}", error));
}
_close(fd);

A word of caution: this will only work if you are using the same C runtime as all of the code that is using cin/stdin. If for example you use the msvcrt.dll as the C runtime for your project, but you are using functions from a DLL that uses msvcr120.dll itself, that DLL will not see the change, because the different C runtime it uses will track a different internal file descriptor. If you compile this code and the code that reads data from cin with the same compiler, this shouldn't be an issue, but if you run third-party code that you don't explicitly recompile with your compiler, I don't know of an easy way around that kind of limitation.

Disclaimer: I've not tested this explicitly, I have similar code for replacing stderr that I've adapted here. I may have made some mistakes translating it.

chris_se
  • 1,006
  • 1
  • 7
  • While it's certainly better not to mix C and C++ IO together, but the [reference](https://en.cppreference.com/w/cpp/io/ios_base/sync_with_stdio) and the [standard](https://eel.is/c++draft/ios.members.static) do say that by default, we should be able to mix them up. – Cho98 Mar 17 '23 at 03:45