Skip to content

Examples

idimus edited this page Jul 10, 2026 · 15 revisions

Full text of example from tutorial

#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::string help = parser.GetHelp(80);
        std::cout << obj.GetErrorString() << std::endl;
        std::cout << help << std::endl;
    }

    return 0;
}

This example creates a program with the keys -n, --numbers, -s, --some_boring_long_name, -h and --help. The help is auto-generated. The types of numbers and some_boring_long_name are integers, each accepting one or more values.

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 actually terminal cannot get infinite numbers of arguments. In most cases maximum length of all input that terminal can pass is limited with 8 kb.

Full text of example from tutorial but shorter way

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

int main(int argc, char** argv)
{
    auto parser = argparse::ArgumentParser(__FILE__).SetDescription("Description of program");

    parser.AddArgument(argparse::CreateNamedArgument("n", "numbers", argparse::kFromOneToInfiniteArgCount,
        argparse::ArgTypeCast::e_int, false, 
        "some numbers description with some important information for user."));
    parser.AddArgument(argparse::CreateNamedArgument("s", "some_boring_long_name", argparse::kFromOneToInfiniteArgCount,
        argparse::ArgTypeCast::e_int, false, 
        "some_boring_long_name description with some important information for user."));
    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::string help = parser.GetHelp(80);
        std::cout << obj.GetErrorString() << std::endl;
        std::cout << help << std::endl;
    }

    return 0;
}

C++20 keyword style — the same two arguments written with designated initializers (requires C++20; the rest of the program is unchanged):

parser.AddArgument(argparse::CreateNamedArgument({
    .longName = "numbers",
    .nargs    = argparse::kFromOneToInfiniteArgCount,
    .type     = argparse::ArgTypeCast::e_int}));
parser.AddArgument(argparse::CreateNamedArgument({
    .longName = "some_boring_long_name",
    .nargs    = argparse::kFromOneToInfiniteArgCount,
    .type     = argparse::ArgTypeCast::e_int,
    .required = false,
    .help     = "some_boring_long_name description with some important information for user."}));

Making 1 required argument, with 1 required value

the generic -h, --help option is suppressed example compiles for c++17 and uses std::any approach

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

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

    parser.AddArgument(argparse::CreateNamedArgument("b", "b_key", 1,  argparse::ArgTypeCast::e_int, true, 
        R"=(some "b_key" description with some important information for user)="));

    parser.SetAddHelp(false);

    auto obj = parser.ParseArgs(argc, argv);
    if (obj.IsArgValid())
    {
        auto arg = obj.GetArg("b_key");
        std::cout << arg.Get().type().name() << ": " << std::any_cast<int>(arg.Get()) << std::endl;
    }
    else
    {
        std::string help = parser.GetHelp(80);
        std::cout << obj.GetErrorString() << std::endl;
        std::cout << help << std::endl;
    }

    return 0;
}

You can change the 1 to 0 in this example. It will then be a flag, and you cannot access its content.

C++20 keyword style (same behaviour):

parser.AddArgument(argparse::CreateNamedArgument({
    .shortName = "b",
    .longName  = "b_key",
    .nargs     = 1,
    .type      = argparse::ArgTypeCast::e_int,
    .required  = true,
    .help      = R"=(some "b_key" description with some important information for user)="}));

Adding positional argument

A positional argument is passed as a raw argument without any key. That is the only difference between keyed and positional arguments.

#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())
    {
        auto arg = obj.GetArg("int1");
        if (arg.GetArgumentExists())
        {
            std::cout << arg.Get().type().name() << ": " << std::any_cast<int>(arg.Get()) << std::endl;
        }
    }
    else
    {
        std::string help = parser.GetHelp(80);
        std::cout << obj.GetErrorString() << std::endl;
        std::cout << help << std::endl;
    }

    return 0;
}

C++20 keyword style (same behaviour):

parser.AddArgument(argparse::CreatePositionalArgument({
    .name     = "int1",
    .type     = argparse::ArgTypeCast::e_int,
    .required = false}));

Simple polish notation calc

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

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

    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 example of epilogue. Will be placed in the end of help");

    auto obj = parser.ParseArgs(argc, argv);
    if (obj.IsArgValid())
    {
        auto arg = obj.GetArg("nums");
        auto op = obj.GetArg("operation");

        const std::vector<double>& nums = arg.GetAsVecDouble();
        const std::string& operation = op.GetAsString();
        std::cout << nums[0] << operation << nums[1] << "=";
        if (operation == "+")
        {
            std::cout << nums[0] + nums[1] << std::endl;
        }
        else if (operation == "-")
        {
            std::cout << nums[0] - nums[1] << std::endl;

        }
        else if (operation == "*")
        {
            std::cout << nums[0] * nums[1] << std::endl;
        }
        else //if(operation == "/")
        {
            std::cout << nums[0] / nums[1] << std::endl;

        }
    }
    else
    {
        std::string help = parser.GetHelp(80);
        std::cout << obj.GetErrorString() << std::endl;
        std::cout << help << std::endl;
    }

    return 0;
}
usage examples
>>> calc.exe 12121 222 -o +
12121+222=12343

>>> calc.exe 12121 222 -o %
Value '%' is out of choices for "operation"
main nums [nums nums] -o,--operation [{+, -, *, /}] [-h,--help]
ArgParse example

positional arguments:

nums                    Type: DOUBLE. Args count: 2


named arguments:

-o,--operation          Type: STRING. Choices:+, -, *, /. Args count: 1
-h,--help               Show help!

This is example of epilogue. Will be placed in the end of help

C++20 keyword style (same behaviour). SetChoices is not part of the spec struct, so chain it afterwards; use an explicit std::vector<std::string> to disambiguate the overload when calling it on the freshly-built argument:

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>{"+", "-", "*", "/"}));

Flags and default values

A flag holds no value — it is either present or not — created with SetArgumentIsFlag(). Remember that 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 when the user omits it.

#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.GetArg("jobs").GetAsInt(); // 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 (same behaviour). A flag is just nargs = 0; SetDefault is chained since it is not a spec field:

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));

Mixing positional and named arguments

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

Important: getters that return a reference — GetAsString() and the GetAsVec*() family — return a reference into the ArgumentParsed object. Store the result of GetArg(...) in a variable before calling them, otherwise the reference dangles. (Value getters like GetAsInt()/GetAsDouble() return by value and are always safe.)

#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;
    }

    // Store the ArgumentParsed before reading string references from it.
    auto sourceArg = obj.GetArg("source");
    auto destArg = obj.GetArg("dest");
    const std::string& src = sourceArg.GetAsString();
    const std::string& dst = destArg.GetAsString();
    const bool force = obj.GetArg("force").GetArgumentExists();

    std::cout << "copy " << src << " -> " << dst << (force ? " (force)" : "") << std::endl;
    return 0;
}
>>> mycp a.txt b.txt -f
copy a.txt -> b.txt (force)

C++20 keyword style (same behaviour):

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"}));

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). These are never thrown; they are reported through the result object 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.GetArg("port").GetAsInt() << std::endl;
    return 0;
}
>>> app -p 8080
listening on port 8080

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

app -p,--port [p] [-h,--help]

named arguments:

-p,--port                Port to listen on Type: INT. Args count: 1
-h,--help                Show help!

C++20 keyword style (same behaviour):

parser.AddArgument(argparse::CreateNamedArgument({
    .shortName = "p", .longName = "port",
    .nargs = 1, .type = argparse::ArgTypeCast::e_int, .required = true,
    .help = "Port to listen on"}));

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;
    }

    auto fromArg = obj.GetArg("from");
    auto toArg = obj.GetArg("to");
    const double v = obj.GetArg("value").GetAsDouble(); // returns by value: safe inline
    const std::string& from = fromArg.GetAsString();
    const std::string& to = toArg.GetAsString();

    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 (same behaviour; SetChoices chained as before):

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"}));

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 (same behaviour):

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.GetArg("name").GetAsString() << "!" << std::endl;
    return 0;
}
>>> greet --name World
Hello, World!

C++20 keyword style (same behaviour; the spec factory lives in your renamed namespace too, here cli::):

parser.AddArgument(cli::CreateNamedArgument({
    .shortName = "n", .longName = "name",
    .nargs = 1, .type = cli::ArgTypeCast::e_String, .required = true,
    .help = "Who to greet"}));

Boolean arguments

Use ArgTypeCast::e_bool for arguments whose value is a boolean. The accepted spellings are true/True/TRUE and false/False/FALSE. (If you want a valueless on/off switch rather than a typed value, use SetArgumentIsFlag() — see Flags and default values 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.GetArg("debug").GetAsBool() ? "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 (same behaviour; SetDefault(false) chained):

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"}));

Overriding the usage line with SetUsage

By default the usage line is generated from your arguments. SetUsage(...) replaces just that first line with your own wording; the positional/named 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;
    }

    auto host = obj.GetArg("host");
    std::cout << "listening on " << host.GetAsString()
              << ":" << obj.GetArg("port").GetAsInt() << 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

named arguments:

-p,--port                Port to listen on Type: INT. Args count: 1
-H,--host                Interface to bind Type: STRING. Args count: 1
-h,--help                Show help!

C++20 keyword style (same behaviour; SetDefault chained):

parser.AddArgument(argparse::CreateNamedArgument({
    .shortName = "p", .longName = "port",
    .nargs = 1, .type = argparse::ArgTypeCast::e_int, .required = true,
    .help = "Port to listen on"}));
parser.AddArgument(argparse::CreateNamedArgument({
    .shortName = "H", .longName = "host",
    .nargs = 1, .type = argparse::ArgTypeCast::e_String, .required = false,
    .help = "Interface to bind"})
    .SetDefault(std::string("0.0.0.0")));

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.

C++20: processing parsed values with ranges

The library compiles cleanly under C++11, C++14, C++17, C++20 and C++23. The value getters (GetAsVecInt() and friends) return by value, so they compose directly with C++20 range views — no dangling, no manual copies.

// Build with C++20:  c++ -std=c++20 -I<path> stats.cpp
#include <iostream>
#include <ranges>
#include <vector>
#include "ArgParse/argparse.h"

int main(int argc, char** argv)
{
    auto parser = argparse::ArgumentParser("stats").SetDescription("C++20 ranges over parsed values");

    parser.AddArgument(argparse::CreateNamedArgument("n", "nums",
        argparse::kFromOneToInfiniteArgCount, argparse::ArgTypeCast::e_int, true)
        .SetHelp("Integers to process"));

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

    auto nums = obj.GetArg("nums").GetAsVecInt();

    // Keep even numbers and square them, lazily, via C++20 views.
    auto evenSquares = nums
        | std::views::filter([](int x) { return x % 2 == 0; })
        | std::views::transform([](int x) { return x * x; });

    std::cout << "even squares:";
    for (int v : evenSquares)
        std::cout << ' ' << v;
    std::cout << std::endl;
    return 0;
}
>>> stats --nums 1 2 3 4 5 6
even squares: 4 16 36

C++20: keyword arguments (designated initializers)

CreateNamedArgument and CreatePositionalArgument also accept an aggregate spec (NamedArgSpec / PositionalArgSpec). With C++20 designated initializers this reads like Python's add_argument(type=..., required=...) — you name each field and skip the ones you don't need, instead of remembering positional argument order.

// Build with C++20:  c++ -std=c++20 -I<path> greet.cpp
#include <iostream>
#include "ArgParse/argparse.h"

int main(int argc, char** argv)
{
    auto parser = argparse::ArgumentParser("greet").SetDescription("Keyword-style arguments (C++20)");

    parser.AddArgument(argparse::CreateNamedArgument({
        .shortName = "n",
        .longName  = "name",
        .nargs     = 1,
        .type      = argparse::ArgTypeCast::e_String,
        .required  = true,
        .help      = "Who to greet"}));

    parser.AddArgument(argparse::CreateNamedArgument({
        .longName  = "count",
        .type      = argparse::ArgTypeCast::e_int,
        .required  = false,
        .help      = "How many times"}));

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

    auto name = obj.GetArg("name");
    auto countArg = obj.GetArg("count");
    int count = countArg.GetArgumentExists() ? countArg.GetAsInt() : 1;
    for (int i = 0; i < count; ++i)
        std::cout << "Hello, " << name.GetAsString() << "!\n";
    return 0;
}
>>> greet --name World --count 2
Hello, World!
Hello, World!

Note: the field order in the designated initializer must follow the struct's declaration order (shortName, longName, nargs, type, required, help for NamedArgSpec). Fields you omit take their defaults. The same structs also work with ordinary aggregate initialization in C++11/14/17.