Skip to content

Examples

simfeo edited this page Aug 6, 2026 · 18 revisions

Practical, copy-pasteable examples for FancyArgumentParser. Every example compiles as-is against the single header (#include "ArgParse/argparse.h") and works from C++11 onward. Where C++20 lets you write the same thing more nicely (designated-initializer "keyword" syntax), it is tucked into a collapsible C++20 keyword style block right under the baseline version — so both are always available without doubling the page.

Looking for features that only exist in C++20 (the import argparse; module, std::ranges post-processing, the keyword-spec deep dive)? See the C++20 features page.

Contents


Hello world

The smallest useful program: one required named argument, then read it back.

#include <iostream>
#include "ArgParse/argparse.h"

int main(int argc, char** argv)
{
    auto parser = argparse::ArgumentParser("greet").SetDescription("A tiny greeter");
    parser.AddArgument(argparse::CreateNamedArgument("n", "name", 1,
        argparse::ArgTypeCast::e_String, true).SetHelp("Who to greet"));

    auto obj = parser.ParseArgs(argc, argv);
    if (!obj.IsArgValid())
    {
        std::cout << obj.GetErrorString() << "\n" << parser.GetHelp(80) << std::endl;
        return 1;
    }

    std::cout << "Hello, " << obj.GetAsString("name") << "!" << std::endl;
    return 0;
}
>>> greet --name World
Hello, World!

>>> greet
Required argument with name "name" does not exist
...auto-generated help...
C++20 keyword style
parser.AddArgument(argparse::CreateNamedArgument({
    .shortName = "n", .longName = "name",
    .nargs = 1, .type = argparse::ArgTypeCast::e_String,
    .required = true, .help = "Who to greet"}));

Named arguments and auto-generated help

A fuller version with two integer options that each take one or more values. The -h/--help option and the whole help text are generated for you.

#include <iostream>
#include "ArgParse/argparse.h"

int main(int argc, char** argv)
{
    auto parser = argparse::ArgumentParser("Program name").SetDescription("Description of program");
    parser.AddArgument(argparse::CreateNamedArgument()
        .SetLongName("numbers")
        .SetAnyNumberOfArgumentsButAtLeastOne()
        .SetType(argparse::ArgTypeCast::e_int));
    parser.AddArgument(argparse::CreateNamedArgument()
        .SetLongName("some_boring_long_name")
        .SetAnyNumberOfArgumentsButAtLeastOne()
        .SetType(argparse::ArgTypeCast::e_int)
        .SetHelp("some_boring_long_name description with some important information for user.")
        .SetRequired(false));

    auto obj = parser.ParseArgs(argc, argv);
    if (obj.IsArgValid())
    {
        auto arg = obj.GetArg("numbers");
        if (arg.GetArgumentExists())
            for (auto& el : arg.GetAsVecInt())
                std::cout << el << std::endl;
    }
    else
    {
        std::cout << obj.GetErrorString() << std::endl;
        std::cout << parser.GetHelp(80) << std::endl;
    }
    return 0;
}

This creates the keys -n/--numbers, -s/--some_boring_long_name and -h/--help. Typical output without any arguments:

main.cpp [-n,--numbers [n ...] ] [-s,--some_boring_long_name [s ...] ] -h,--help
Description of program

optional arguments:

-n,--numbers            some numbers description with some important information
                        for user. Type: INT. Args count: at least one.
-s,--some_boring_long_name
                        some_boring_long_name description with some important information
                        for user. Type: INT. Args count: at least one.
-h,--help               Show help!

Note: a terminal cannot really pass an infinite number of arguments — most shells cap the whole command line at around 8 kB.

Shorter ways to declare an argument

The fluent setters above are one of three equivalent styles. You can also pass everything positionally to the factory, or (in C++20) name the fields.

// Positional factory arguments (C++11+):
parser.AddArgument(argparse::CreateNamedArgument("n", "numbers",
    argparse::kFromOneToInfiniteArgCount, argparse::ArgTypeCast::e_int, false,
    "some numbers description with some important information for user."));
C++20 keyword style
parser.AddArgument(argparse::CreateNamedArgument({
    .longName = "numbers",
    .nargs    = argparse::kFromOneToInfiniteArgCount,
    .type     = argparse::ArgTypeCast::e_int,
    .required = false,
    .help     = "some numbers description with some important information for user."}));

Short names may be written as a single char ('n') as well as a string ("n").

Positional arguments

A positional argument is passed as a raw value without any key — that is the only difference from a named argument.

#include <iostream>
#include "ArgParse/argparse.h"

int main(int argc, char** argv)
{
    auto parser = argparse::ArgumentParser("main").SetDescription("ArgParse example");

    parser.AddArgument(argparse::CreatePositionalArgument("int1")
          .SetType(argparse::ArgTypeCast::e_int).SetRequired(false));

    auto obj = parser.ParseArgs(argc, argv);
    if (obj.IsArgValid() && obj.GetArg("int1").GetArgumentExists())
        std::cout << "int1 = " << obj.GetAsInt("int1") << std::endl;
    else if (!obj.IsArgValid())
        std::cout << obj.GetErrorString() << "\n" << parser.GetHelp(80) << std::endl;
    return 0;
}
C++20 keyword style
parser.AddArgument(argparse::CreatePositionalArgument({
    .name = "int1", .type = argparse::ArgTypeCast::e_int, .required = false}));

Mixing positional and named arguments

Positional and named arguments can be combined freely and passed in any order. Below, a small cp-like tool takes two positionals (source, dest) and one named flag (-f/--force).

#include <iostream>
#include "ArgParse/argparse.h"

int main(int argc, char** argv)
{
    auto parser = argparse::ArgumentParser("mycp").SetDescription("Copy SOURCE to DEST");

    parser.AddArgument(argparse::CreatePositionalArgument("source").SetHelp("File to copy from"));
    parser.AddArgument(argparse::CreatePositionalArgument("dest").SetHelp("File to copy to"));
    parser.AddArgument(argparse::CreateNamedArgument("f", "force").SetArgumentIsFlag()
        .SetRequired(false).SetHelp("Overwrite destination if it exists"));

    auto obj = parser.ParseArgs(argc, argv);
    if (!obj.IsArgValid())
    {
        std::cout << obj.GetErrorString() << "\n" << parser.GetHelp(80) << std::endl;
        return 1;
    }

    const std::string src = obj.GetAsString("source");   // by value: always safe
    const std::string dst = obj.GetAsString("dest");
    const bool force = obj.GetArg("force").GetArgumentExists();

    std::cout << "copy " << src << " -> " << dst << (force ? " (force)" : "") << std::endl;
    return 0;
}
>>> mycp a.txt --force b.txt
copy a.txt -> b.txt (force)
C++20 keyword style
parser.AddArgument(argparse::CreatePositionalArgument({.name = "source", .help = "File to copy from"}));
parser.AddArgument(argparse::CreatePositionalArgument({.name = "dest", .help = "File to copy to"}));
parser.AddArgument(argparse::CreateNamedArgument({
    .shortName = "f", .longName = "force",
    .nargs = 0, .required = false, .help = "Overwrite destination if it exists"}));

How many values: nargs

nargs controls how many values an argument consumes. You can pass an exact integer, one of the named constants, or a Python-style character.

You want Character Constant
Exactly N N (an int)
Zero or one '?' kZeroOrOneArgCount
Zero or more '*' kAnyArgCount
One or more '+' kFromOneToInfiniteArgCount
// '?'  zero-or-one (falls back to the default when omitted)
parser.AddArgument(argparse::CreateNamedArgument("c", "count", '?')
    .SetType(argparse::ArgTypeCast::e_int).SetDefault(1));

// '+'  one-or-more values
parser.AddArgument(argparse::CreatePositionalArgument("files", '+'));

// '*'  zero-or-more values
parser.AddArgument(argparse::CreateNamedArgument("x", "extra", '*'));
C++20 keyword style
parser.AddArgument(argparse::CreateNamedArgument({
    .shortName = "c", .longName = "count", .nargs = '?',
    .type = argparse::ArgTypeCast::e_int}).SetDefault(1));
parser.AddArgument(argparse::CreatePositionalArgument({.name = "files", .nargs = '+'}));

Choices and validators

Restrict what values an argument accepts. SetChoices limits it to a fixed set; the validators check each value with a rule and fail the parse (with a message) when it doesn't hold.

#include <iostream>
#include <string>
#include "ArgParse/argparse.h"

int main(int argc, char** argv)
{
    auto parser = argparse::ArgumentParser("srv").SetDescription("Validators demo");

    // Fixed set of allowed values.
    parser.AddArgument(argparse::CreateNamedArgument("o", "op").SetRequired(false)
        .SetChoices({"+", "-", "*", "/"}));

    // Numeric range (also sets the type for you).
    parser.AddArgument(argparse::CreateNamedArgument("p", "port").SetRequired(false)
        .SetRange(1, 65535));

    // Strictly positive.
    parser.AddArgument(argparse::CreateNamedArgument("r", "ratio").SetRequired(false)
        .SetType(argparse::ArgTypeCast::e_double).SetPositive());

    // Any custom predicate, with an optional error message.
    parser.AddArgument(argparse::CreateNamedArgument("m", "mode").SetRequired(false)
        .SetValidator([](const std::string& v){ return v == "fast" || v == "safe"; },
                      "mode must be 'fast' or 'safe'"));

    auto obj = parser.ParseArgs(argc, argv);
    if (!obj.IsArgValid())
    {
        std::cout << "error: " << obj.GetErrorString() << std::endl;
        return 1;
    }
    std::cout << "ok" << std::endl;
    return 0;
}
>>> srv --port 70000
error: value out of range [1, 65535]

>>> srv --mode turbo
error: mode must be 'fast' or 'safe'

Built-in validators: SetRange (int / long long / double, as (lo, hi) or (max)), SetPositive, SetNonNegative, SetPattern (regex), and the filesystem checks SetExistingFile, SetExistingDirectory, SetExistingPath, SetNonexistentPath (available when <filesystem> is, i.e. C++17+).

A predicate and its message can also be passed straight to the factory or a spec:

// Positional factory: ...help, predicate, message
parser.AddArgument(argparse::CreatePositionalArgument("name", 1,
    argparse::ArgTypeCast::e_String, true, "a name",
    [](const std::string& s){ return s == "ok"; }, "name must be 'ok'"));
C++20 keyword style

SetChoices is not a spec field, so chain it afterwards (use an explicit std::vector<std::string> to disambiguate the overload). The validator, however, is a spec field:

parser.AddArgument(argparse::CreateNamedArgument({
    .shortName = "o", .longName = "op", .required = false})
    .SetChoices(std::vector<std::string>{"+", "-", "*", "/"}));

parser.AddArgument(argparse::CreateNamedArgument({
    .shortName = "m", .longName = "mode", .required = false,
    .validator = [](const std::string& v){ return v == "fast" || v == "safe"; },
    .validator_message = "mode must be 'fast' or 'safe'"}));

Flags and default values

A flag holds no value — it is either present or not — created with SetArgumentIsFlag() (equivalently nargs = 0). Every argument is required by default, so an optional flag must opt out with SetRequired(false). SetDefault(...) gives an optional argument a value to fall back on.

#include <iostream>
#include "ArgParse/argparse.h"

int main(int argc, char** argv)
{
    auto parser = argparse::ArgumentParser("build").SetDescription("Flags and default values");

    parser.AddArgument(argparse::CreateNamedArgument("v", "verbose").SetArgumentIsFlag()
        .SetRequired(false).SetHelp("Enable verbose output"));
    parser.AddArgument(argparse::CreateNamedArgument("j", "jobs", 1, argparse::ArgTypeCast::e_int, false)
        .SetDefault(1).SetHelp("Number of parallel jobs (default: 1)"));

    auto obj = parser.ParseArgs(argc, argv);
    if (!obj.IsArgValid())
    {
        std::cout << obj.GetErrorString() << "\n" << parser.GetHelp(80) << std::endl;
        return 1;
    }

    const bool verbose = obj.GetArg("verbose").GetArgumentExists();
    const int jobs = obj.GetAsInt("jobs"); // always present thanks to SetDefault

    std::cout << "verbose = " << (verbose ? "true" : "false") << "\n";
    std::cout << "jobs    = " << jobs << std::endl;
    return 0;
}
>>> build -v
verbose = true
jobs    = 1

>>> build
verbose = false
jobs    = 1
C++20 keyword style
parser.AddArgument(argparse::CreateNamedArgument({
    .shortName = "v", .longName = "verbose",
    .nargs = 0, .required = false, .help = "Enable verbose output"}));
parser.AddArgument(argparse::CreateNamedArgument({
    .shortName = "j", .longName = "jobs",
    .type = argparse::ArgTypeCast::e_int, .required = false,
    .help = "Number of parallel jobs (default: 1)"}).SetDefault(1));

Boolean arguments

Use ArgTypeCast::e_bool for arguments whose value is a boolean. The accepted spellings are true/True/TRUE and false/False/FALSE. (For a valueless on/off switch, use SetArgumentIsFlag() instead — see Flags above.)

#include <iostream>
#include "ArgParse/argparse.h"

int main(int argc, char** argv)
{
    auto parser = argparse::ArgumentParser("feature").SetDescription("Bool-typed arguments");

    parser.AddArgument(argparse::CreateNamedArgument("d", "debug", 1,
        argparse::ArgTypeCast::e_bool, false).SetDefault(false)
        .SetHelp("Enable debug mode (true/false)"));

    // A bool argument can also take several values.
    parser.AddArgument(argparse::CreateNamedArgument("s", "switches",
        argparse::kFromOneToInfiniteArgCount, argparse::ArgTypeCast::e_bool, false)
        .SetHelp("A series of on/off switches"));

    auto obj = parser.ParseArgs(argc, argv);
    if (!obj.IsArgValid())
    {
        std::cout << obj.GetErrorString() << "\n" << parser.GetHelp(80) << std::endl;
        return 1;
    }

    std::cout << "debug = " << (obj.GetAsBool("debug") ? "true" : "false") << "\n";

    auto sw = obj.GetArg("switches");
    if (sw.GetArgumentExists())
    {
        std::cout << "switches =";
        for (bool b : sw.GetAsVecBool())
            std::cout << ' ' << (b ? "on" : "off");
        std::cout << std::endl;
    }
    return 0;
}
>>> feature --debug true --switches true false TRUE
debug = true
switches = on off on
C++20 keyword style
parser.AddArgument(argparse::CreateNamedArgument({
    .shortName = "d", .longName = "debug",
    .nargs = 1, .type = argparse::ArgTypeCast::e_bool, .required = false,
    .help = "Enable debug mode (true/false)"}).SetDefault(false));
parser.AddArgument(argparse::CreateNamedArgument({
    .shortName = "s", .longName = "switches",
    .nargs = argparse::kFromOneToInfiniteArgCount, .type = argparse::ArgTypeCast::e_bool,
    .required = false, .help = "A series of on/off switches"}));

Reading results by name

After a successful parse you can read values straight off the result by argument name, without going through GetArg(...) first:

int                       n     = obj.GetAsInt("count");
double                    ratio = obj.GetAsDouble("ratio");
std::string               name  = obj.GetAsString("name");
std::vector<int>          nums  = obj.GetAsVecInt("numbers");

These by-name getters return by value (a copy), so — unlike GetArg(name).GetAsString() which returns a reference into the parsed object — there is no lifetime dance: you can call them inline and store the result however you like. Reach for the longer GetArg("count") form when you need the Argument itself, e.g. to check GetArgumentExists() on an optional argument.

// safe, self-contained:
const std::string host = obj.GetAsString("host");

// also fine, but note GetAsString() here returns a reference into 'a':
auto a = obj.GetArg("host");
const std::string& hostRef = a.GetAsString();

Variable binding with BindTo

Instead of reading each value back, you can bind an argument to one of your variables with BindTo(&var). After a successful parse the value lands there automatically. BindTo also infers the argument's type from the variable, so no SetType call is needed. Available from C++11.

#include <iostream>
#include <string>
#include <vector>
#include "ArgParse/argparse.h"

int main(int argc, char** argv)
{
    // Bound variables. Initial values act as defaults for optional arguments.
    std::string host = "localhost";
    int         port = 8080;
    bool        verbose = false;
    std::vector<int> workers;

    auto parser = argparse::ArgumentParser("serve").SetDescription("Start a server");

    parser.AddArgument(argparse::CreateNamedArgument("H", "host", 1)
        .SetRequired(false).SetHelp("Bind address").BindTo(&host));
    parser.AddArgument(argparse::CreateNamedArgument("p", "port", 1)
        .SetRequired(false).SetHelp("Port to listen on").BindTo(&port));
    parser.AddArgument(argparse::CreateNamedArgument("v", "verbose", 1)
        .SetRequired(false).SetHelp("Verbose logging").BindTo(&verbose));
    parser.AddArgument(argparse::CreateNamedArgument("w", "workers")
        .SetRequired(false).SetAnyNumberOfArgumentsButAtLeastOne()
        .SetHelp("Worker ids").BindTo(&workers));

    auto obj = parser.ParseArgs(argc, argv);
    if (!obj.IsArgValid())
    {
        std::cout << obj.GetErrorString() << "\n" << parser.GetHelp(80) << std::endl;
        return 1;
    }

    // No GetArg(...).GetAsX() anywhere — the variables are already populated.
    std::cout << "host=" << host << " port=" << port
              << " verbose=" << std::boolalpha << verbose << "\n";
    std::cout << "workers (" << workers.size() << "):";
    for (int id : workers) std::cout << " " << id;
    std::cout << "\n";
    return 0;
}
>>> serve --host 0.0.0.0 --port 9090 --verbose true --workers 1 2 3
host=0.0.0.0 port=9090 verbose=true
workers (3): 1 2 3

>>> serve
host=localhost port=8080 verbose=false
workers (0):

Note: the bound variable must outlive the ParseArgs call. If an optional argument is absent (and has no default), its bound variable is left untouched — so its initializer acts as the default. Bindings are applied only on a successful parse.

Configuring the parser with ParserSpec

The parser-level options (description, prefixChars, addHelp, allowAbbrev, ignoreUnknownArgs, …) can be set in one place by constructing the parser from a ParserSpec, instead of chaining setters.

#include "ArgParse/argparse.h"

int main(int argc, char** argv)
{
    argparse::ParserSpec spec;
    spec.name        = "cptool";
    spec.description = "Copy files";
    spec.allowAbbrev = false;

    auto parser = argparse::ArgumentParser(spec);
    // ... AddArgument(...) as usual ...
    auto obj = parser.ParseArgs(argc, argv);
    return obj.IsArgValid() ? 0 : 1;
}
C++20 keyword style

With designated initializers this reads like Python's ArgumentParser(...):

auto parser = argparse::ArgumentParser(argparse::ParserSpec{
    .name        = "cptool",
    .description = "Copy files",
    .allowAbbrev = false});

Variable argument count, custom prefix and ignore-unknown

kAnyArgCount accepts zero or more values (use kFromOneToInfiniteArgCount to demand at least one). SetPrefixChars('+') changes the option prefix, and SetIgnoreUnknownArgs(true) lets the parser skip options it doesn't recognise instead of failing.

#include <iostream>
#include "ArgParse/argparse.h"

int main(int argc, char** argv)
{
    auto parser = argparse::ArgumentParser("sum")
        .SetDescription("Sum any amount of numbers")
        .SetPrefixChars('+')
        .SetIgnoreUnknownArgs(true);

    parser.AddArgument(argparse::CreateNamedArgument("n", "nums", argparse::kAnyArgCount,
        argparse::ArgTypeCast::e_int, false).SetHelp("Numbers to add"));

    auto obj = parser.ParseArgs(argc, argv);
    if (!obj.IsArgValid())
    {
        std::cout << obj.GetErrorString() << "\n" << parser.GetHelp(80) << std::endl;
        return 1;
    }

    long long total = 0;
    auto arg = obj.GetArg("nums");
    if (arg.GetArgumentExists())
        for (int n : arg.GetAsVecInt())
            total += n;

    std::cout << "sum = " << total << std::endl;
    return 0;
}
>>> sum ++nums 3 4 5 ++junk hello
sum = 12
C++20 keyword style
parser.AddArgument(argparse::CreateNamedArgument({
    .shortName = "n", .longName = "nums",
    .nargs = argparse::kAnyArgCount, .type = argparse::ArgTypeCast::e_int,
    .required = false, .help = "Numbers to add"}));

Using a custom namespace

If the default argparse namespace clashes with something in your project, define ARGPARSE_NAMESPACE_NAME before including the header to rename it.

#define ARGPARSE_NAMESPACE_NAME cli
#include <iostream>
#include "ArgParse/argparse.h"

int main(int argc, char** argv)
{
    auto parser = cli::ArgumentParser("greet").SetDescription("Custom namespace demo");
    parser.AddArgument(cli::CreateNamedArgument("n", "name", 1, cli::ArgTypeCast::e_String, true)
        .SetHelp("Who to greet"));

    auto obj = parser.ParseArgs(argc, argv);
    if (!obj.IsArgValid())
    {
        std::cout << obj.GetErrorString() << "\n" << parser.GetHelp(80) << std::endl;
        return 1;
    }
    std::cout << "Hello, " << obj.GetAsString("name") << "!" << std::endl;
    return 0;
}
>>> greet --name World
Hello, World!

Overriding the usage line

By default the usage line is generated from your arguments. SetUsage(...) replaces just that first line with your own wording; the argument listings below it are still generated for you.

#include <iostream>
#include "ArgParse/argparse.h"

int main(int argc, char** argv)
{
    auto parser = argparse::ArgumentParser("serve").SetDescription("Start a web server");
    parser.SetUsage("serve --port PORT [--host HOST]");

    parser.AddArgument(argparse::CreateNamedArgument("p", "port", 1,
        argparse::ArgTypeCast::e_int, true).SetHelp("Port to listen on"));
    parser.AddArgument(argparse::CreateNamedArgument("H", "host", 1,
        argparse::ArgTypeCast::e_String, false).SetDefault(std::string("0.0.0.0"))
        .SetHelp("Interface to bind"));

    auto obj = parser.ParseArgs(argc, argv);
    if (!obj.IsArgValid())
    {
        std::cout << obj.GetErrorString() << "\n" << parser.GetHelp(80) << std::endl;
        return 1;
    }

    std::cout << "listening on " << obj.GetAsString("host")
              << ":" << obj.GetAsInt("port") << std::endl;
    return 0;
}
>>> serve --port 8080
listening on 0.0.0.0:8080

>>> serve
Required argument with name "port" does not exist
serve --port PORT [--host HOST]
Start a web server
...

Tip: put positional arguments before named ones on the command line. The parser collects positionals first, so mytool FILE --flag works while mytool --flag FILE may misassign FILE.

Error handling patterns

There are two distinct kinds of errors:

  • Setup errors — a malformed definition (e.g. an argument with neither a name nor a positional name). AddArgument throws for these; they are programmer mistakes. You only need a try/catch if you build arguments dynamically from external data.
  • Input errors — bad user input (missing required argument, wrong type, value out of choices, failed validator). These are never thrown; they are reported through the result via IsArgValid() and GetErrorString().
#include <iostream>
#include "ArgParse/argparse.h"

int main(int argc, char** argv)
{
    argparse::ArgumentParser parser("app");

    try
    {
        parser.AddArgument(argparse::CreateNamedArgument("p", "port", 1,
            argparse::ArgTypeCast::e_int, true).SetHelp("Port to listen on"));
    }
    catch (const std::exception& e)
    {
        std::cerr << "argument setup error: " << e.what() << std::endl;
        return 2;
    }

    auto obj = parser.ParseArgs(argc, argv);
    if (!obj.IsArgValid())
    {
        std::cerr << "error: " << obj.GetErrorString() << "\n\n";
        std::cerr << parser.GetHelp(80) << std::endl;
        return 1;
    }

    std::cout << "listening on port " << obj.GetAsInt("port") << std::endl;
    return 0;
}
>>> app -p 8080
listening on port 8080

>>> app
error: Required argument with name "port" does not exist
...

Real-world example: temperature converter

Combines a typed positional (double), two required named arguments constrained with SetChoices, and an epilogue.

#include <iostream>
#include "ArgParse/argparse.h"

int main(int argc, char** argv)
{
    auto parser = argparse::ArgumentParser("convert").SetDescription("Convert a temperature between units");

    parser.AddArgument(argparse::CreatePositionalArgument("value")
        .SetType(argparse::ArgTypeCast::e_double).SetHelp("Temperature value"));
    parser.AddArgument(argparse::CreateNamedArgument("f", "from").SetRequired(true)
        .SetChoices({"C", "F", "K"}).SetHelp("Source unit"));
    parser.AddArgument(argparse::CreateNamedArgument("t", "to").SetRequired(true)
        .SetChoices({"C", "F", "K"}).SetHelp("Target unit"));
    parser.SetEpilogue("Units: C = Celsius, F = Fahrenheit, K = Kelvin");

    auto obj = parser.ParseArgs(argc, argv);
    if (!obj.IsArgValid())
    {
        std::cout << obj.GetErrorString() << "\n" << parser.GetHelp(80) << std::endl;
        return 1;
    }

    const double v = obj.GetAsDouble("value");
    const std::string from = obj.GetAsString("from");
    const std::string to = obj.GetAsString("to");

    double c = from == "C" ? v : from == "F" ? (v - 32.0) * 5.0 / 9.0 : v - 273.15;
    double out = to == "C" ? c : to == "F" ? c * 9.0 / 5.0 + 32.0 : c + 273.15;

    std::cout << v << from << " = " << out << to << std::endl;
    return 0;
}
>>> convert 100 -f C -t F
100C = 212F

>>> convert 100 -f C -t Q
Value 'Q' is out of choices for "to"
C++20 keyword style

SetChoices is chained since it is not a spec field:

parser.AddArgument(argparse::CreatePositionalArgument({
    .name = "value", .type = argparse::ArgTypeCast::e_double, .help = "Temperature value"}));
parser.AddArgument(argparse::CreateNamedArgument({
    .shortName = "f", .longName = "from", .required = true, .help = "Source unit"})
    .SetChoices(std::vector<std::string>{"C", "F", "K"}));
parser.AddArgument(argparse::CreateNamedArgument({
    .shortName = "t", .longName = "to", .required = true, .help = "Target unit"})
    .SetChoices(std::vector<std::string>{"C", "F", "K"}));

Simple Polish-notation calculator

Two positional numbers and an operator constrained by SetChoices.

#include <iostream>
#include "ArgParse/argparse.h"

int main(int argc, char** argv)
{
    auto parser = argparse::ArgumentParser("calc").SetDescription("Polish-notation calc");

    parser.AddArgument(argparse::CreatePositionalArgument("nums")
        .SetType(argparse::ArgTypeCast::e_double).SetNumberOfArguments(2));
    parser.AddArgument(argparse::CreateNamedArgument("o", "operation")
        .SetRequired(true).SetChoices({"+", "-", "*", "/"}));
    parser.SetEpilogue("This is an example of an epilogue, placed at the end of the help.");

    auto obj = parser.ParseArgs(argc, argv);
    if (!obj.IsArgValid())
    {
        std::cout << obj.GetErrorString() << "\n" << parser.GetHelp(80) << std::endl;
        return 1;
    }

    const std::vector<double> nums = obj.GetAsVecDouble("nums");
    const std::string op = obj.GetAsString("operation");
    std::cout << nums[0] << op << nums[1] << "=";
    if      (op == "+") std::cout << nums[0] + nums[1] << std::endl;
    else if (op == "-") std::cout << nums[0] - nums[1] << std::endl;
    else if (op == "*") std::cout << nums[0] * nums[1] << std::endl;
    else                std::cout << nums[0] / nums[1] << std::endl;
    return 0;
}
>>> calc 12121 222 -o +
12121+222=12343

>>> calc 12121 222 -o %
Value '%' is out of choices for "operation"
C++20 keyword style
parser.AddArgument(argparse::CreatePositionalArgument({
    .name = "nums", .nargs = 2, .type = argparse::ArgTypeCast::e_double}));
parser.AddArgument(argparse::CreateNamedArgument({
    .shortName = "o", .longName = "operation", .required = true})
    .SetChoices(std::vector<std::string>{"+", "-", "*", "/"}));

Want the C++20-only capabilities — consuming the library as a module, keyword specs in depth, or piping parsed values through std::ranges? Continue to C++20 features.