【问题标题】:Why is calling a class method in the constructor causing the program to hang?为什么在构造函数中调用类方法会导致程序挂起?
【发布时间】:2023-03-07 14:41:01
【问题描述】:

我正在尝试使用 Qt 创建一个 GUI 来将 Rip Grep 包装为一个项目,以便更加熟悉 Qt、C++ 和 Win32 API。我有一个名为RunCommand 的类,我正在编写包含运行命令行程序并将其输出捕获到字符串以供在GUI 中使用的功能。在RunCommand 中,我有一些方法可以封装RunCommand 必须做的工作的不同部分。我试图让构造函数通过调用方法来完成所有工作。问题是,当我从构造函数调用方法时,程序无限期挂起,但是当我将方法中的代码直接复制并粘贴到构造函数中时,事情按预期工作。我在这里错过了什么?

我注意到如果将方法直接粘贴到构造函数中,代码运行良好。 RunCommand 在用户单击helloButton 时启动。我有一条消息正在写给QTextEdit 小部件以进行调试,我已将它放在构造函数调用的类方法中,然后该方法中的任何实际代码运行然后立即返回,但我仍然遇到挂起,所以我认为问题在于我调用该方法的方式,而不在于该方法试图做什么。如果我不从构造函数调用类方法,我可以将其他文本写入QTextEdit 小部件。

easyrip.cpp

#include "easyrip.h"
#include "ui_easyrip.h"
#include "runcommand.h"
#include "synchapi.h"

using namespace std;

EasyRip::EasyRip(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::EasyRip)
{
    ui->setupUi(this);

    connect(ui->helloButton, &QPushButton::pressed, this, &EasyRip::testHello);
}

EasyRip::~EasyRip()
{
    delete ui;
}

void EasyRip::testHello()
{
    ui->cmdDisplay->setText("Running rg help command...");
    string cmdOutput = "";
    RunCommand(R"(C:\Users\Name\OneDrive\RipGrep\rg.exe --help)", cmdOutput);
    ui->cmdDisplay->setText(cmdOutput.c_str());
}

运行命令.h

#ifndef CMDRUNNER_H
#define CMDRUNNER_H

#include <Windows.h> 
#include <tchar.h>
#include <stdio.h> 
#include <strsafe.h>
#include <string>

class RunCommand
{
public:
    RunCommand(const std::string cmd, std::string& cmdOutput);
    ~RunCommand();

private:
    void CreateChildProcess(const std::string cmd, std::string& cmdOutput);
    void ReadFromPipe(std::string& cmdOutput);

    HANDLE _hChildStd_OUT_Rd = nullptr;
    HANDLE _hChildStd_OUT_Wr = nullptr;
};

#endif

运行命令.cpp

#include "runcommand.h"

constexpr int BUFSIZE = 4096;

using namespace std;

RunCommand::~RunCommand()
{
   CloseHandle(_hChildStd_OUT_Rd);
   CloseHandle(_hChildStd_OUT_Wr);
}

// Runs cmd and returns the command output on cmdOutput.
RunCommand::RunCommand(const string cmd, string& cmdOutput)
{
   // Set the bInheritHandle flag so pipe handles are inherited by the child process.
   SECURITY_ATTRIBUTES saAttr;
   saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
   saAttr.bInheritHandle = TRUE;
   saAttr.lpSecurityDescriptor = nullptr;

   // Create a pipe for the child process's STDOUT.
   if (!CreatePipe(&_hChildStd_OUT_Rd, &_hChildStd_OUT_Wr, &saAttr, 0))
   {
      cmdOutput.assign("Error: StdoutRd CreatePipe failed");
      return;
   }

   // Ensure the read handle to the pipe for STDOUT is not inherited.
   // We want the child process to only inherit the write end of the PIPE
   // we created above. Then it can write to the inherited write end of 
   // the PIPE, and we can read from the non-inherited read end.
   if (!SetHandleInformation(_hChildStd_OUT_Rd, HANDLE_FLAG_INHERIT, FALSE))
   {
      cmdOutput.assign("Error: Stdout SetHandleInformation failed");
      return;
   }

   // If "Debug point" is assigned to cmdOutput here and returned, 
   // the text is successfully displayed in the QTextEdit widget,
   // and the code does not hang.

   // Create the child process.
   // I have also tried calling this as RunCommand::CreateChildProcess
   this->CreateChildProcess(cmd, cmdOutput);

   // Read the standard output from the child process.
   this->ReadFromPipe(cmdOutput);
}

// Create a child process that uses the previously created pipes for STDOUT.
void RunCommand::CreateChildProcess(const string cmd, string& cmdOutput)
{
   cmdOutput.assign("Debug point"); // Issue: Never reaches this line.
   return;

   ...
}

// Read output from the child process's pipe for STDOUT
// and copy it to the referenced std::string.
// Stop when there is no more data. 
void RunCommand::ReadFromPipe(std::string& cmdOutput)
{
   ...
}

预期结果:rg --help 命令的内容被复制到 cmdOutput 字符串,然后显示在 QTextEdit 小部件中。

实际结果:程序无限挂起,需要在RunCommand构造函数尝试调用类方法时强制关闭。

任何建议都非常感谢,谢谢。

【问题讨论】:

  • SECURITY_ATTRIBUTES saAttr; -- 您未能初始化saAttr。您现在使用的是半生不熟的saAttr 并将其传递给 Windows API。请改用SECURITY_ATTRIBUTES saAttr {};
  • 我试图让构造函数通过调用方法来完成所有工作。 -- 这不是一个好主意,IMO。构造函数应该做最少的工作。
  • 我可能会拥有 SECURITY_ATTRIBUTES 作为成员,设置值,并且可能拥有作为成员运行的命令。然后将调用RunCommand()::run() 成员函数来完成这项工作。至于初始化,我看到SECURITY_ATTRIBUTES 只有 3 个成员,但为了安全起见,你不应该这样编码。始终对 Windows API 结构进行零初始化,因为您永远不知道结构何时可以添加成员或以某种方式更改。在其他 Windows API 结构中,您不能只设置您关心的成员。
  • 从你在“easyrip.cpp”中使用这个类的方式来看,我建议你真正需要的是实现一个“静态成员函数”。静态成员函数根本不需要实例化类,这样可以解决你遇到的构造函数挂起,同时也暗示对象不能多次“Run()”。
  • 我想我以前也遇到过类似的问题。问题似乎是类构造函数在对象尚未完全构造的对象的生命周期内执行。构造函数方法的重点是执行使对象存在所需的所有初始化。调用要求对象完全启动且可用的成员函数是一种需要避免的反模式。

标签: c++ windows qt winapi


【解决方案1】:

为了跟进,我最终发现问题不是由调用构造函数中的方法引起的。这样做在技术上没问题,即使它不是推荐的方法。

真正的问题发生在ReadFromPipe。由于某种原因,子进程在运行后没有退出,这导致正在读取的逻辑永远挂起,等待某种文件结束信号。我从未在 UI 中看到"Debug point",因为即使我在设置文本后立即从CreateChildProcess 返回,我也忘记了也立即从ReadFromPipe 返回。代码挂在ReadFromPipe 中意味着我从未在 UI 中看到调试文本。

感谢所有关于如何改进的有用建议,它们对帮助我找出问题所在非常有用。

【讨论】:

    猜你喜欢
    • 2015-04-29
    • 1970-01-01
    • 2013-04-19
    • 2015-03-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多