5

*emphasized text*How can I use Boost program options to accept single-byte variables from the command line?

Command line parameters of --id1=1 --id2=1 results in values of id1=49 (or '1', 0x31) and id2=1.

#include <stdint.h>
#include <iostream>
#include <boost/program_options.hpp>

using namespace std;

int main(int argc, char** argv)
{
    (void)argc;
    (void)argv;
    namespace po = boost::program_options;

    const int myargc = 3;
    const char* myargv[] = {"foo","--id1=1","--id2=2" };

    uint8_t  id1;
    uint16_t id2; // works as expected.

    po::variables_map vm;
    po::options_description cmd_options( "Command options" );
    cmd_options.add_options()
    ( "id1", po::value<uint8_t >( &id1 )->default_value( 0 ), "A 1-byte ID" )
    ( "id2", po::value<uint16_t>( &id2 )->default_value( 0 ), "A 2-byte ID" )
    ;

    po::store( po::parse_command_line( myargc, myargv, cmd_options ), vm );
    po::notify( vm );
    // Using command line parameters of --id1=1 --id2=1,    
    // at this point, id1=49 (or '1', 0x31) and id2=1.
    cout << "BPO parsing of " << myargv[1] << " and " << myargv[2] << endl;
    cout << "id1: " <<      id1 << endl;
    cout << "id1: " << (int)id1 << endl;
    cout << "id2: " <<      id2 << endl;

    id1 = boost::lexical_cast<uint8_t>("1");
    id2 = boost::lexical_cast<int>("2");

    cout << "Using boost::lexical_cast" << endl;
    cout << "id1: " <<      id1 << endl;
    cout << "id1: " << (int)id1 << endl;
    cout << "id2: " <<      id2 << endl;

}

output is:

BPO parsing of --id1=1 and --id2=2
id1: 1
id1: 49
id2: 2
Using boost::lexical_cast
id1: 1
id1: 49
id2: 2

Boost eventually calls boost::lexical_cast("1")' which converts as a char rather than a numeric value - a "1" becomes a '1' which is 49.

Is there a way to change the boost::program_options::add_options() initialization to treat single-bye values as ints rather than string/char? If not, what options do I have to change the parsing or mapping? Obvious (but unfavorable) options are: [1] don't use char-like values [2] manually parse (bypass Boost) or [3] perform a secondary conversion after Boost does its parsing.

Jim Fred
  • 1,076
  • 13
  • 27
  • which platform and toolchain? – Sam Miller Feb 03 '13 at 21:32
  • I discovered this on Win7-64 with VC2012 but this is also targeted for Linux X86 and Linux ARM with gcc. I'm thinking that it doesn't matter - that it's not a platform-specific bug but rather the way streaming works and that any solutions would work cross-platform. – Jim Fred Feb 04 '13 at 10:35
  • 1
    [I tried your reproducer](http://liveworkspace.org/code/1iAkAH$0) and see the expected results. I see the same behavior on ppc64 Linux and Mac OS X. Perhaps you could elaborate on your environment? – Sam Miller Feb 04 '13 at 17:27
  • Sam: Thanks. Your code reproduces the problem and I edited my post using your code example with casts added to make the problem more obvious. I get the same results on Windows and Linux x86 and it's caused by the boost::lexical_cast. BTW, thanks for the tip about LiveWorkSpace. – Jim Fred Feb 06 '13 at 07:15
  • I guess it's because `uint8_t` is probably a typedef to char. Maybe file this as a bug? – RedX Feb 06 '13 at 07:24
  • RedX: uint8_t is an unsigned char. I'd guess that there's a way to influence or override boost::program_options casting. Meanwhile, I'll see how people handle hex values with boost::program_options. That might give me a clue. – Jim Fred Feb 06 '13 at 07:34

2 Answers2

3

The behavior you observe has nothing to do with boost::program_options or boost::lexical_cast, it's just how stream extraction for uint8_t types works. The following demonstrates that

#include <iostream>
#include <sstream>

int
main()
{
   uint8_t id1;
   uint16_t id2;
   std::istringstream iss( "1 1" );
   iss >> id1 >> id2;
   std::cout << "id1 char:     " << id1 << std::endl;
   std::cout << "id1 uint8_t:  " << (uint8_t)id1 << std::endl;
   std::cout << "id1 int:      " << (int)id1 << std::endl;
   std::cout << "id2 uint16_t: " << (uint16_t)id2 << std::endl;
   std::cout << "id2 int:      " << (int)id2 << std::endl;
}

produces:

id1 char:     1
id1 uint8_t:  1
id1 int:      49
id2 uint16_t: 1
id2 int:      1

If you want to limit your program options to a single byte, I suggest using boost::numeric_cast

#include <stdint.h>
#include <iostream> 
#include <boost/bind.hpp>
#include <boost/numeric/conversion/cast.hpp>
#include <boost/program_options.hpp>

int
main(int argc, char** argv)
{
    (void)argc;
    (void)argv;
    namespace po = boost::program_options;

    const int myargc = 3;
    const char* myargv[] = {"foo","--id1=1","--id2=2" };

    unsigned  id1;
    uint16_t id2; // works as expected.

    po::options_description cmd_options( "Command options" );
    cmd_options.add_options()
        ( "id1", po::value<unsigned>( &id1 )->notifier([](unsigned value){ boost::numeric_cast<uint8_t>(value); } ), "A 1-byte ID" )
        ( "id2", po::value<uint16_t>( &id2 ), "A 2-byte ID" )
        ;

    po::variables_map vm;
    po::store( po::parse_command_line( myargc, myargv, cmd_options ), vm );
    po::notify( vm );
    std::cout << "id1: " << id1 << std::endl;
    std::cout << "id2: " << id2 << std::endl;
}

here is a sample session

Sam Miller
  • 23,808
  • 4
  • 67
  • 87
  • But, id1 is no longer a single-byte value. – Jim Fred Feb 06 '13 at 21:00
  • @Jim correct, it's an integer that accepts values from `0-255` – Sam Miller Feb 06 '13 at 21:34
  • Sam: I guess I did ask for single-byte values. Sorry to move the cheese but I changed the question from 'single-byte values' to 'single-byte variables'. – Jim Fred Feb 07 '13 at 00:43
  • 1
    @JimFred: OK, so the answer to your question is, "Stop using single-byte variables". Is it really that hard to just copy the value into a byte later? Or just use a bigger size of data? It's not hurting anyone; your program isn't going to implode because you used a `uint16_t` instead of a `uint8_t`. – Nicol Bolas Feb 07 '13 at 05:44
  • Nicol: Size would matter in a communications packet. As you say (and as noted in the original post) a secondary conversion or copy could be done. I was looking for options to influence Boost's conversion. – Jim Fred Feb 07 '13 at 06:12
3

Create a numeric byte class that is the size of a byte but streams like a numeric value rather than streaming like a char.

#include <stdint.h>
#include <iostream>
#include <boost/program_options.hpp>

using namespace std;

struct NumByte
{
    uint8_t value;

    NumByte() : value() {}
    NumByte( const uint8_t &arg ) : value(arg) {}
    NumByte( const NumByte &arg ) : value(arg.value) {}

    operator uint8_t() const { return value; }

    friend istream& operator>>(istream& in, NumByte& valArg)
    {
        int i;
        in >> i;
        valArg.value = static_cast<uint8_t>(i);
        return in;
    }

    friend ostream& operator<<(ostream& out, NumByte& valArg)
    {
        out << static_cast<int>(valArg.value);
        return out;
    }
};

int main(int argc, char** argv)
{
    (void)argc;
    (void)argv;
    namespace po = boost::program_options;

    const int myargc = 3;
    const char* myargv[] = {"foo","--id1=1","--id2=2" };

    NumByte  id1;
    uint16_t id2; 

    po::variables_map vm;
    po::options_description cmd_options( "Command options" );
    cmd_options.add_options()
      ( "id1", po::value<NumByte >( &id1 )->default_value( 0 ), "A 1-byte ID" )
      ( "id2", po::value<uint16_t>( &id2 )->default_value( 0 ), "A 2-byte ID" )
      ;

    po::store( po::parse_command_line( myargc, myargv, cmd_options ), vm );
    po::notify( vm );

    assert( sizeof(NumByte)==1 ); // insure the size of a numeric byte is the size of a byte.

    cout << "BPO parsing of " << myargv[1] << " and " << myargv[2] << endl;
    cout << "id1: " <<      id1 << endl;
    cout << "id1: " << (int)id1 << endl;
    cout << "id2: " <<      id2 << endl;

    id1 = boost::lexical_cast<NumByte>("1");
    id2 = boost::lexical_cast<int>("2");

    cout << "Using boost::lexical_cast" << endl;
    cout << "id1: " <<      id1 << endl;
    cout << "id1: " << (int)id1 << endl;
    cout << "id2: " <<      id2 << endl;

}

output is:

BPO parsing of --id1=1 and --id2=2
id1: 1
id1: 1
id2: 2
Using boost::lexical_cast
id1: 1
id1: 1
id2: 2
Jim Fred
  • 1,076
  • 13
  • 27