🏡 rpn_manual.nim

《现代 C++ 手把手实现 RPN 计算器》

目录

  1. 第 1 章:Hello RPN
  2. 第 2 章:头文件化 + 单元测试
  3. 第 3 章:交互式 REPL
  4. 第 4 章:单文件发布 & 交叉编译
  5. 第 5 章:课后挑战


第 1 章:Hello RPN


先给出一个最小可运行版本,保存为 rpn_min.cpp


rpn_min.cpp

#include <iostream>
#include <sstream>
#include <stack>
#include <stdexcept>

double rpn(const std::string& line){
    std::stack<double> st;
    std::istringstream iss(line);
    std::string tok;
    while (iss >> tok){
        if (tok == "+" || tok == "-" || tok == "*" || tok == "/"){
            if (st.size() < 2) throw std::runtime_error("need 2 operands");
            double b = st.top(); st.pop();
            double a = st.top(); st.pop();
            switch (tok[0]){
                case '+': st.push(a + b); break;
                case '-': st.push(a - b); break;
                case '*': st.push(a * b); break;
                case '/':
                    if (b == 0) throw std::runtime_error("divide by 0");
                    st.push(a / b); break;
            }
        } else {
            st.push(std::stod(tok));
        }
    }
    if (st.size() != 1) throw std::runtime_error("stack size != 1");
    return st.top();
}

int main(){
    std::cout << "RPN> ";
    std::string line;
    std::getline(std::cin, line);
    try {
        std::cout << "Result: " << rpn(line) << '\n';
    } catch (const std::exception& ex){
        std::cerr << "Error: " << ex.what() << '\n';
    }
}

编译 & 运行:


第 2 章:头文件化 + 单元测试



rpn_engine.hpp

#pragma once
#include <stack>
#include <sstream>
#include <stdexcept>
#include <string>

double evaluateRPN(const std::string& expr){
    std::stack<double> st;
    std::istringstream iss(expr);
    std::string tok;
    while (iss >> tok){
        if (tok.size() == 1 && std::string("+-*/").find(tok[0]) != std::string::npos){
            if (st.size() < 2) throw std::runtime_error("stack underflow");
            double b = st.top(); st.pop();
            double a = st.top(); st.pop();
            switch (tok[0]){
                case '+': st.push(a + b); break;
                case '-': st.push(a - b); break;
                case '*': st.push(a * b); break;
                case '/':
                    if (b == 0) throw std::runtime_error("divide by zero");
                    st.push(a / b); break;
            }
        } else {
            st.push(std::stod(tok));
        }
    }
    if (st.size() != 1) throw std::runtime_error("single value expected");
    return st.top();
}


test.cpp

#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest/doctest.h"
#include "rpn_engine.hpp"

TEST_CASE("basic"){
    CHECK(evaluateRPN("3 4 +") == 7);
    CHECK(evaluateRPN("2 3 4 + *") == 14);
    CHECK(evaluateRPN("10 2 /") == 5);
}
TEST_CASE("exceptions"){
    CHECK_THROWS(evaluateRPN("1 +"));
    CHECK_THROWS(evaluateRPN("3 0 /"));
    CHECK_THROWS(evaluateRPN("1 2 3"));
}

下载 doctest 单文件头库并执行测试:


第 3 章:交互式 REPL


借助 linenoise 单文件头库实现行编辑与历史。


repl.cpp

#include "rpn_engine.hpp"
#include "linenoise.hpp"
#include <iomanip>

int main(){
    const char* prompt = "rpn> ";
    std::string line;
    while (linenoise::Readline(prompt, line)){
        if (line == "exit" || line == "quit") break;
        try {
            double v = evaluateRPN(line);
            std::cout << "= " << std::setprecision(12) << v << '\n';
            linenoise::AddHistory(line.c_str());
        } catch (const std::exception& ex){
            std::cerr << "error: " << ex.what() << '\n';
        }
    }
}


第 4 章:单文件发布 & 交叉编译


把全部源码合并成 rpn_single.cpp-Os -s -DNDEBUG 优化后仅 ~92 KB(UPX 后)。


Linux→Windows 交叉编译


第 5 章:课后挑战


  1. 增加幂运算 (^) 与单目负号
  2. 支持变量存储 (sto / rcl)
  3. std::variant 实现高精度有理数,避免浮点误差
  4. 编译为 WebAssembly,在浏览器里运行!

祝 hacking 愉快!

# ************ 0. 前置 import **********************************
import std/[strutils, os, sequtils]
import nimib
import ./lib/codeOutput          # 会真正用到,保证无 UnusedImport

# ************ 1. 目录 & 章节模板 *******************************
var nbToc: NbBlock

template addToc =
  newNbBlock("nbText", false, nb, nbToc, ""):
    nbToc.output = "## 目录\n\n"

template nbNewSection(name: string) =
  let anch = name.toLower.replace(" ", "-")
  nbText: "<a name=\"" & anch & "\"></a>\n<br>\n### " & name & "\n\n---"
  nbToc.output.add "1. <a href=\"#" & anch & "\">" & name & "</a>\n"

template nbSubSection(name: string) =
  let anch = name.toLower.replace(" ", "-")
  nbText: "<a name=\"" & anch & "\"></a>\n<br>\n#### " & name & "\n\n"
  nbToc.output.add "    * <a href=\"#" & anch & "\">" & name & "</a>\n"

# ************ 2. 主题美化 *************************************
template initCodeTheme* =
  nb.context["stylesheet"] = """
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css">"""
  nb.context["highlight"] = """
<link rel="stylesheet" media="(prefers-color-scheme: dark)"
  href="https://cdn.jsdelivr.net/gh/highlightjs/highlight.js/src/styles/github-dark.css">
<link rel="stylesheet" media="(prefers-color-scheme: light)"
  href="https://cdn.jsdelivr.net/gh/highlightjs/highlight.js/src/styles/github.css">"""
  nbRawHtml: """
<style>
pre code{border-radius:6px}pre{border:1px solid #809eb7;border-radius:6px 6px 0 0;margin-bottom:0}
pre.nb-output{border:1px solid #809eb7;border-radius:0 0 6px 6px;border-top:none;padding:.8em;margin-top:0;overflow-x:auto;background:#f8f8f8}
</style>"""

# ************ 3. 文档开始 ***************************************
nbInit()
initCodeTheme()

nbText: """
# 《现代 C++ 手把手实现 RPN 计算器》

- 版本:1.0
- 编译器:GCC 10+ / Clang 12+ / MSVC 2019+
- 标准:C++20
"""

addToc()

# =================================================================
nbNewSection("第 1 章:Hello RPN")
# =================================================================
nbText: """
先给出一个**最小可运行**版本,保存为 `rpn_min.cpp`。
"""

nbSubSection("rpn_min.cpp")
nbRawHtml: """
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;sstream&gt;
#include &lt;stack&gt;
#include &lt;stdexcept&gt;

double rpn(const std::string& line){
    std::stack&lt;double&gt; st;
    std::istringstream iss(line);
    std::string tok;
    while (iss >> tok){
        if (tok == "+" || tok == "-" || tok == "*" || tok == "/"){
            if (st.size() < 2) throw std::runtime_error("need 2 operands");
            double b = st.top(); st.pop();
            double a = st.top(); st.pop();
            switch (tok[0]){
                case '+': st.push(a + b); break;
                case '-': st.push(a - b); break;
                case '*': st.push(a * b); break;
                case '/':
                    if (b == 0) throw std::runtime_error("divide by 0");
                    st.push(a / b); break;
            }
        } else {
            st.push(std::stod(tok));
        }
    }
    if (st.size() != 1) throw std::runtime_error("stack size != 1");
    return st.top();
}

int main(){
    std::cout << "RPN> ";
    std::string line;
    std::getline(std::cin, line);
    try {
        std::cout << "Result: " << rpn(line) << '\n';
    } catch (const std::exception& ex){
        std::cerr << "Error: " << ex.what() << '\n';
    }
}
</code></pre>
"""

nbText: """
编译 & 运行:
"""



# =================================================================
nbNewSection("第 2 章:头文件化 + 单元测试")
# =================================================================
nbSubSection("rpn_engine.hpp")
nbRawHtml: """
<pre><code class="language-cpp">#pragma once
#include &lt;stack&gt;
#include &lt;sstream&gt;
#include &lt;stdexcept&gt;
#include &lt;string&gt;

double evaluateRPN(const std::string& expr){
    std::stack&lt;double&gt; st;
    std::istringstream iss(expr);
    std::string tok;
    while (iss >> tok){
        if (tok.size() == 1 && std::string("+-*/").find(tok[0]) != std::string::npos){
            if (st.size() < 2) throw std::runtime_error("stack underflow");
            double b = st.top(); st.pop();
            double a = st.top(); st.pop();
            switch (tok[0]){
                case '+': st.push(a + b); break;
                case '-': st.push(a - b); break;
                case '*': st.push(a * b); break;
                case '/':
                    if (b == 0) throw std::runtime_error("divide by zero");
                    st.push(a / b); break;
            }
        } else {
            st.push(std::stod(tok));
        }
    }
    if (st.size() != 1) throw std::runtime_error("single value expected");
    return st.top();
}
</code></pre>
"""

nbSubSection("test.cpp")
nbRawHtml: """
<pre><code class="language-cpp">#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest/doctest.h"
#include "rpn_engine.hpp"

TEST_CASE("basic"){
    CHECK(evaluateRPN("3 4 +") == 7);
    CHECK(evaluateRPN("2 3 4 + *") == 14);
    CHECK(evaluateRPN("10 2 /") == 5);
}
TEST_CASE("exceptions"){
    CHECK_THROWS(evaluateRPN("1 +"));
    CHECK_THROWS(evaluateRPN("3 0 /"));
    CHECK_THROWS(evaluateRPN("1 2 3"));
}
</code></pre>
"""

nbText: """
下载 **doctest** 单文件头库并执行测试:
"""

# =================================================================
nbNewSection("第 3 章:交互式 REPL")
# =================================================================
nbText: """
借助 `linenoise` 单文件头库实现行编辑与历史。
"""

nbSubSection("repl.cpp")
nbRawHtml: """
<pre><code class="language-cpp">#include "rpn_engine.hpp"
#include "linenoise.hpp"
#include &lt;iomanip&gt;

int main(){
    const char* prompt = "rpn> ";
    std::string line;
    while (linenoise::Readline(prompt, line)){
        if (line == "exit" || line == "quit") break;
        try {
            double v = evaluateRPN(line);
            std::cout << "= " << std::setprecision(12) << v << '\n';
            linenoise::AddHistory(line.c_str());
        } catch (const std::exception& ex){
            std::cerr << "error: " << ex.what() << '\n';
        }
    }
}
</code></pre>
"""


# =================================================================
nbNewSection("第 4 章:单文件发布 & 交叉编译")
# =================================================================
nbText: """
把全部源码合并成 `rpn_single.cpp`,`-Os -s -DNDEBUG` 优化后仅 ~92 KB(UPX 后)。
"""

nbSubSection("Linux→Windows 交叉编译")

# =================================================================
nbNewSection("第 5 章:课后挑战")
# =================================================================
nbText: """
1. 增加幂运算 (`^`) 与单目负号  
2. 支持变量存储 (`sto` / `rcl`)  
3. 用 `std::variant` 实现高精度有理数,避免浮点误差  
4. 编译为 **WebAssembly**,在浏览器里运行!

祝 hacking 愉快!
"""

# =================================================================
nbSave()
nbText: """
"""  # 单独一行,顶格写
# 再留一个空行