Commit 17ddce2fb2e76d538ec449c395ae9ae0a36ca215

Authored by Philip Top
Committed by Henry Schreiner
1 parent ba7aac9c

Add classification type traits (#286)

This cleans up the type checking a bit and makes it more readable, along with some other cleanup.

* start work on trying to clean up the type traits for which lexical cast overload to use

* fix readme issue and make the condition tests a little clearer

* add a check for out of range errors on boolean conversions

* Fix capitalization and some comments on option functions

* fix a few code analysis warnings for VS2019
README.md
... ... @@ -604,7 +604,7 @@ The subcommand method
604 604 .add_option_group(name,description)
605 605 ```
606 606  
607   -Will create an option group, and return a pointer to it. An option group allows creation of a collection of options, similar to the groups function on options, but with additional controls and requirements. They allow specific sets of options to be composed and controlled as a collective. For an example see [range test](./tests/ranges.cpp). Option groups are a specialization of an App so all [functions](#subcommand-options) that work with an App or subcommand also work on option groups. Options can be created as part of an option group using the add functions just like a subcommand, or previously created options can be added through
  607 +Will create an option group, and return a pointer to it. An option group allows creation of a collection of options, similar to the groups function on options, but with additional controls and requirements. They allow specific sets of options to be composed and controlled as a collective. For an example see [range example](https://github.com/CLIUtils/CLI11/blob/master/examples/ranges.cpp). Option groups are a specialization of an App so all [functions](#subcommand-options) that work with an App or subcommand also work on option groups. Options can be created as part of an option group using the add functions just like a subcommand, or previously created options can be added through
608 608  
609 609 ```cpp
610 610 ogroup->add_option(option_pointer);
... ...
include/CLI/App.hpp
... ... @@ -1292,7 +1292,7 @@ class App {
1292 1292 }
1293 1293  
1294 1294 std::vector<std::string> args;
1295   - args.reserve(static_cast<size_t>(argc - 1));
  1295 + args.reserve(static_cast<size_t>(argc) - 1);
1296 1296 for(int i = argc - 1; i > 0; i--)
1297 1297 args.emplace_back(argv[i]);
1298 1298 parse(std::move(args));
... ... @@ -2317,7 +2317,7 @@ class App {
2317 2317  
2318 2318 // LCOV_EXCL_START
2319 2319 default:
2320   - HorribleError("unrecognized classifier (you should not see this!)");
  2320 + throw HorribleError("unrecognized classifier (you should not see this!)");
2321 2321 // LCOV_EXCL_END
2322 2322 }
2323 2323 return retval;
... ...
include/CLI/Option.hpp
... ... @@ -527,7 +527,7 @@ class Option : public OptionBase&lt;Option&gt; {
527 527 return this;
528 528 }
529 529  
530   - /// disable flag overrides
  530 + /// Disable flag overrides values, e.g. --flag=<value> is not allowed
531 531 Option *disable_flag_override(bool value = true) {
532 532 disable_flag_override_ = value;
533 533 return this;
... ... @@ -564,7 +564,7 @@ class Option : public OptionBase&lt;Option&gt; {
564 564 /// Get the short names
565 565 const std::vector<std::string> get_snames() const { return snames_; }
566 566  
567   - /// get the flag names with specified default values
  567 + /// Get the flag names with specified default values
568 568 const std::vector<std::string> get_fnames() const { return fnames_; }
569 569  
570 570 /// The number of times the option expects to be included
... ... @@ -790,6 +790,7 @@ class Option : public OptionBase&lt;Option&gt; {
790 790 return (detail::find_member(name, fnames_, ignore_case_, ignore_underscore_) >= 0);
791 791 }
792 792  
  793 + /// Get the value that goes for a flag, nominally gets the default value but allows for overrides if not disabled
793 794 std::string get_flag_value(std::string name, std::string input_value) const {
794 795 static const std::string trueString{"true"};
795 796 static const std::string falseString{"false"};
... ... @@ -855,7 +856,7 @@ class Option : public OptionBase&lt;Option&gt; {
855 856 /// Get a copy of the results
856 857 std::vector<std::string> results() const { return results_; }
857 858  
858   - /// get the results as a particular type
  859 + /// Get the results as a specified type
859 860 template <typename T,
860 861 enable_if_t<!is_vector<T>::value && !std::is_const<T>::value, detail::enabler> = detail::dummy>
861 862 void results(T &output) const {
... ... @@ -884,7 +885,7 @@ class Option : public OptionBase&lt;Option&gt; {
884 885 throw ConversionError(get_name(), results_);
885 886 }
886 887 }
887   - /// get the results as a vector of a particular type
  888 + /// Get the results as a vector of the specified type
888 889 template <typename T> void results(std::vector<T> &output) const {
889 890 output.clear();
890 891 bool retval = true;
... ... @@ -899,7 +900,7 @@ class Option : public OptionBase&lt;Option&gt; {
899 900 }
900 901 }
901 902  
902   - /// return the results as a particular type
  903 + /// Return the results as the specified type
903 904 template <typename T> T as() const {
904 905 T output;
905 906 results(output);
... ... @@ -980,7 +981,7 @@ class Option : public OptionBase&lt;Option&gt; {
980 981 }
981 982  
982 983 private:
983   - // run through the validators
  984 + // Run a result through the validators
984 985 std::string _validate(std::string &result) {
985 986 std::string err_msg;
986 987 for(const auto &vali : validators_) {
... ... @@ -995,6 +996,7 @@ class Option : public OptionBase&lt;Option&gt; {
995 996 return err_msg;
996 997 }
997 998  
  999 + /// Add a single result to the result set, taking into account delimiters
998 1000 int _add_result(std::string &&result) {
999 1001 int result_count = 0;
1000 1002 if(delimiter_ == '\0') {
... ...
include/CLI/TypeTools.hpp
... ... @@ -228,6 +228,118 @@ std::string checked_to_string(T &amp;&amp;) {
228 228 return std::string{};
229 229 }
230 230  
  231 +// Enumeration of the different supported categorizations of objects
  232 +enum objCategory : int {
  233 + integral_value = 2,
  234 + unsigned_integral = 4,
  235 + enumeration = 6,
  236 + boolean_value = 8,
  237 + floating_point = 10,
  238 + number_constructible = 12,
  239 + double_constructible = 14,
  240 + integer_constructible = 16,
  241 + vector_value = 30,
  242 + // string assignable or greater used in a condition so anything string like must come last
  243 + string_assignable = 50,
  244 + string_constructible = 60,
  245 + other = 200,
  246 +
  247 +};
  248 +
  249 +/// some type that is not otherwise recognized
  250 +template <typename T, typename Enable = void> struct classify_object { static constexpr objCategory value{other}; };
  251 +
  252 +/// Set of overloads to classify an object according to type
  253 +template <typename T>
  254 +struct classify_object<T,
  255 + typename std::enable_if<std::is_integral<T>::value && std::is_signed<T>::value &&
  256 + !is_bool<T>::value && !std::is_enum<T>::value>::type> {
  257 + static constexpr objCategory value{integral_value};
  258 +};
  259 +
  260 +/// Unsigned integers
  261 +template <typename T>
  262 +struct classify_object<
  263 + T,
  264 + typename std::enable_if<std::is_integral<T>::value && std::is_unsigned<T>::value && !is_bool<T>::value>::type> {
  265 + static constexpr objCategory value{unsigned_integral};
  266 +};
  267 +
  268 +/// Boolean values
  269 +template <typename T> struct classify_object<T, typename std::enable_if<is_bool<T>::value>::type> {
  270 + static constexpr objCategory value{boolean_value};
  271 +};
  272 +
  273 +/// Floats
  274 +template <typename T> struct classify_object<T, typename std::enable_if<std::is_floating_point<T>::value>::type> {
  275 + static constexpr objCategory value{floating_point};
  276 +};
  277 +
  278 +/// String and similar direct assignment
  279 +template <typename T>
  280 +struct classify_object<
  281 + T,
  282 + typename std::enable_if<!std::is_floating_point<T>::value && !std::is_integral<T>::value &&
  283 + std::is_assignable<T &, std::string>::value && !is_vector<T>::value>::type> {
  284 + static constexpr objCategory value{string_assignable};
  285 +};
  286 +
  287 +/// String and similar constructible and copy assignment
  288 +template <typename T>
  289 +struct classify_object<
  290 + T,
  291 + typename std::enable_if<!std::is_floating_point<T>::value && !std::is_integral<T>::value &&
  292 + !std::is_assignable<T &, std::string>::value &&
  293 + std::is_constructible<T, std::string>::value && !is_vector<T>::value>::type> {
  294 + static constexpr objCategory value{string_constructible};
  295 +};
  296 +
  297 +/// Enumerations
  298 +template <typename T> struct classify_object<T, typename std::enable_if<std::is_enum<T>::value>::type> {
  299 + static constexpr objCategory value{enumeration};
  300 +};
  301 +
  302 +/// Handy helper to contain a bunch of checks that rule out many common types (integers, string like, floating point,
  303 +/// vectors, and enumerations
  304 +template <typename T> struct uncommon_type {
  305 + using type = typename std::conditional<!std::is_floating_point<T>::value && !std::is_integral<T>::value &&
  306 + !std::is_assignable<T &, std::string>::value &&
  307 + !std::is_constructible<T, std::string>::value && !is_vector<T>::value &&
  308 + !std::is_enum<T>::value,
  309 + std::true_type,
  310 + std::false_type>::type;
  311 + static const bool value = type::value;
  312 +};
  313 +
  314 +/// Assignable from double or int
  315 +template <typename T>
  316 +struct classify_object<T,
  317 + typename std::enable_if<uncommon_type<T>::value && is_direct_constructible<T, double>::value &&
  318 + is_direct_constructible<T, int>::value>::type> {
  319 + static constexpr objCategory value{number_constructible};
  320 +};
  321 +
  322 +/// Assignable from int
  323 +template <typename T>
  324 +struct classify_object<T,
  325 + typename std::enable_if<uncommon_type<T>::value && !is_direct_constructible<T, double>::value &&
  326 + is_direct_constructible<T, int>::value>::type> {
  327 + static const objCategory value{integer_constructible};
  328 +};
  329 +
  330 +/// Assignable from double
  331 +template <typename T>
  332 +struct classify_object<T,
  333 + typename std::enable_if<uncommon_type<T>::value && is_direct_constructible<T, double>::value &&
  334 + !is_direct_constructible<T, int>::value>::type> {
  335 + static const objCategory value{double_constructible};
  336 +};
  337 +
  338 +/// vector type
  339 +template <typename T> struct classify_object<T, typename std::enable_if<is_vector<T>::value>::type> {
  340 + static const objCategory value{vector_value};
  341 +};
  342 +
231 343 // Type name print
232 344  
233 345 /// Was going to be based on
... ... @@ -235,38 +347,45 @@ std::string checked_to_string(T &amp;&amp;) {
235 347 /// But this is cleaner and works better in this case
236 348  
237 349 template <typename T,
238   - enable_if_t<std::is_integral<T>::value && std::is_signed<T>::value, detail::enabler> = detail::dummy>
  350 + enable_if_t<classify_object<T>::value == integral_value || classify_object<T>::value == integer_constructible,
  351 + detail::enabler> = detail::dummy>
239 352 constexpr const char *type_name() {
240 353 return "INT";
241 354 }
242 355  
243   -template <typename T,
244   - enable_if_t<std::is_integral<T>::value && std::is_unsigned<T>::value, detail::enabler> = detail::dummy>
  356 +template <typename T, enable_if_t<classify_object<T>::value == unsigned_integral, detail::enabler> = detail::dummy>
245 357 constexpr const char *type_name() {
246 358 return "UINT";
247 359 }
248 360  
249   -template <typename T, enable_if_t<std::is_floating_point<T>::value, detail::enabler> = detail::dummy>
  361 +template <
  362 + typename T,
  363 + enable_if_t<classify_object<T>::value == floating_point || classify_object<T>::value == number_constructible ||
  364 + classify_object<T>::value == double_constructible,
  365 + detail::enabler> = detail::dummy>
250 366 constexpr const char *type_name() {
251 367 return "FLOAT";
252 368 }
253 369  
254 370 /// This one should not be used, since vector types print the internal type
255   -template <typename T, enable_if_t<is_vector<T>::value, detail::enabler> = detail::dummy>
  371 +template <typename T, enable_if_t<classify_object<T>::value == vector_value, detail::enabler> = detail::dummy>
256 372 constexpr const char *type_name() {
257 373 return "VECTOR";
258 374 }
259 375 /// Print name for enumeration types
260   -template <typename T, enable_if_t<std::is_enum<T>::value, detail::enabler> = detail::dummy>
  376 +template <typename T, enable_if_t<classify_object<T>::value == enumeration, detail::enabler> = detail::dummy>
261 377 constexpr const char *type_name() {
262 378 return "ENUM";
263 379 }
264 380  
  381 +/// Print name for enumeration types
  382 +template <typename T, enable_if_t<classify_object<T>::value == boolean_value, detail::enabler> = detail::dummy>
  383 +constexpr const char *type_name() {
  384 + return "BOOLEAN";
  385 +}
  386 +
265 387 /// Print for all other types
266   -template <typename T,
267   - enable_if_t<!std::is_floating_point<T>::value && !std::is_integral<T>::value && !is_vector<T>::value &&
268   - !std::is_enum<T>::value,
269   - detail::enabler> = detail::dummy>
  388 +template <typename T, enable_if_t<classify_object<T>::value >= string_assignable, detail::enabler> = detail::dummy>
270 389 constexpr const char *type_name() {
271 390 return "TEXT";
272 391 }
... ... @@ -286,6 +405,9 @@ inline int64_t to_flag_value(std::string val) {
286 405 val = detail::to_lower(val);
287 406 int64_t ret;
288 407 if(val.size() == 1) {
  408 + if(val[0] >= '1' && val[0] <= '9') {
  409 + return (static_cast<int64_t>(val[0]) - '0');
  410 + }
289 411 switch(val[0]) {
290 412 case '0':
291 413 case 'f':
... ... @@ -293,22 +415,11 @@ inline int64_t to_flag_value(std::string val) {
293 415 case '-':
294 416 ret = -1;
295 417 break;
296   - case '1':
297 418 case 't':
298 419 case 'y':
299 420 case '+':
300 421 ret = 1;
301 422 break;
302   - case '2':
303   - case '3':
304   - case '4':
305   - case '5':
306   - case '6':
307   - case '7':
308   - case '8':
309   - case '9':
310   - ret = val[0] - '0';
311   - break;
312 423 default:
313 424 throw std::invalid_argument("unrecognized character");
314 425 }
... ... @@ -325,10 +436,7 @@ inline int64_t to_flag_value(std::string val) {
325 436 }
326 437  
327 438 /// Signed integers
328   -template <
329   - typename T,
330   - enable_if_t<std::is_integral<T>::value && std::is_signed<T>::value && !is_bool<T>::value && !std::is_enum<T>::value,
331   - detail::enabler> = detail::dummy>
  439 +template <typename T, enable_if_t<classify_object<T>::value == integral_value, detail::enabler> = detail::dummy>
332 440 bool lexical_cast(const std::string &input, T &output) {
333 441 try {
334 442 size_t n = 0;
... ... @@ -343,9 +451,7 @@ bool lexical_cast(const std::string &amp;input, T &amp;output) {
343 451 }
344 452  
345 453 /// Unsigned integers
346   -template <typename T,
347   - enable_if_t<std::is_integral<T>::value && std::is_unsigned<T>::value && !is_bool<T>::value, detail::enabler> =
348   - detail::dummy>
  454 +template <typename T, enable_if_t<classify_object<T>::value == unsigned_integral, detail::enabler> = detail::dummy>
349 455 bool lexical_cast(const std::string &input, T &output) {
350 456 if(!input.empty() && input.front() == '-')
351 457 return false; // std::stoull happily converts negative values to junk without any errors.
... ... @@ -363,7 +469,7 @@ bool lexical_cast(const std::string &amp;input, T &amp;output) {
363 469 }
364 470  
365 471 /// Boolean values
366   -template <typename T, enable_if_t<is_bool<T>::value, detail::enabler> = detail::dummy>
  472 +template <typename T, enable_if_t<classify_object<T>::value == boolean_value, detail::enabler> = detail::dummy>
367 473 bool lexical_cast(const std::string &input, T &output) {
368 474 try {
369 475 auto out = to_flag_value(input);
... ... @@ -371,11 +477,16 @@ bool lexical_cast(const std::string &amp;input, T &amp;output) {
371 477 return true;
372 478 } catch(const std::invalid_argument &) {
373 479 return false;
  480 + } catch(const std::out_of_range &) {
  481 + // if the number is out of the range of a 64 bit value then it is still a number and for this purpose is still
  482 + // valid all we care about the sign
  483 + output = (input[0] != '-');
  484 + return true;
374 485 }
375 486 }
376 487  
377 488 /// Floats
378   -template <typename T, enable_if_t<std::is_floating_point<T>::value, detail::enabler> = detail::dummy>
  489 +template <typename T, enable_if_t<classify_object<T>::value == floating_point, detail::enabler> = detail::dummy>
379 490 bool lexical_cast(const std::string &input, T &output) {
380 491 try {
381 492 size_t n = 0;
... ... @@ -389,27 +500,21 @@ bool lexical_cast(const std::string &amp;input, T &amp;output) {
389 500 }
390 501  
391 502 /// String and similar direct assignment
392   -template <typename T,
393   - enable_if_t<!std::is_floating_point<T>::value && !std::is_integral<T>::value &&
394   - std::is_assignable<T &, std::string>::value,
395   - detail::enabler> = detail::dummy>
  503 +template <typename T, enable_if_t<classify_object<T>::value == string_assignable, detail::enabler> = detail::dummy>
396 504 bool lexical_cast(const std::string &input, T &output) {
397 505 output = input;
398 506 return true;
399 507 }
400 508  
401 509 /// String and similar constructible and copy assignment
402   -template <typename T,
403   - enable_if_t<!std::is_floating_point<T>::value && !std::is_integral<T>::value &&
404   - !std::is_assignable<T &, std::string>::value && std::is_constructible<T, std::string>::value,
405   - detail::enabler> = detail::dummy>
  510 +template <typename T, enable_if_t<classify_object<T>::value == string_constructible, detail::enabler> = detail::dummy>
406 511 bool lexical_cast(const std::string &input, T &output) {
407 512 output = T(input);
408 513 return true;
409 514 }
410 515  
411 516 /// Enumerations
412   -template <typename T, enable_if_t<std::is_enum<T>::value, detail::enabler> = detail::dummy>
  517 +template <typename T, enable_if_t<classify_object<T>::value == enumeration, detail::enabler> = detail::dummy>
413 518 bool lexical_cast(const std::string &input, T &output) {
414 519 typename std::underlying_type<T>::type val;
415 520 bool retval = detail::lexical_cast(input, val);
... ... @@ -421,12 +526,7 @@ bool lexical_cast(const std::string &amp;input, T &amp;output) {
421 526 }
422 527  
423 528 /// Assignable from double or int
424   -template <typename T,
425   - enable_if_t<!std::is_floating_point<T>::value && !std::is_integral<T>::value &&
426   - !std::is_assignable<T &, std::string>::value &&
427   - !std::is_constructible<T, std::string>::value && !std::is_enum<T>::value &&
428   - is_direct_constructible<T, double>::value && is_direct_constructible<T, int>::value,
429   - detail::enabler> = detail::dummy>
  529 +template <typename T, enable_if_t<classify_object<T>::value == number_constructible, detail::enabler> = detail::dummy>
430 530 bool lexical_cast(const std::string &input, T &output) {
431 531 int val;
432 532 if(lexical_cast(input, val)) {
... ... @@ -442,13 +542,8 @@ bool lexical_cast(const std::string &amp;input, T &amp;output) {
442 542 return from_stream(input, output);
443 543 }
444 544  
445   -/// Assignable from int64
446   -template <typename T,
447   - enable_if_t<!std::is_floating_point<T>::value && !std::is_integral<T>::value &&
448   - !std::is_assignable<T &, std::string>::value &&
449   - !std::is_constructible<T, std::string>::value && !std::is_enum<T>::value &&
450   - !is_direct_constructible<T, double>::value && is_direct_constructible<T, int>::value,
451   - detail::enabler> = detail::dummy>
  545 +/// Assignable from int
  546 +template <typename T, enable_if_t<classify_object<T>::value == integer_constructible, detail::enabler> = detail::dummy>
452 547 bool lexical_cast(const std::string &input, T &output) {
453 548 int val;
454 549 if(lexical_cast(input, val)) {
... ... @@ -459,12 +554,7 @@ bool lexical_cast(const std::string &amp;input, T &amp;output) {
459 554 }
460 555  
461 556 /// Assignable from double
462   -template <typename T,
463   - enable_if_t<!std::is_floating_point<T>::value && !std::is_integral<T>::value &&
464   - !std::is_assignable<T &, std::string>::value &&
465   - !std::is_constructible<T, std::string>::value && !std::is_enum<T>::value &&
466   - is_direct_constructible<T, double>::value && !is_direct_constructible<T, int>::value,
467   - detail::enabler> = detail::dummy>
  557 +template <typename T, enable_if_t<classify_object<T>::value == double_constructible, detail::enabler> = detail::dummy>
468 558 bool lexical_cast(const std::string &input, T &output) {
469 559 double val;
470 560 if(lexical_cast(input, val)) {
... ... @@ -475,15 +565,10 @@ bool lexical_cast(const std::string &amp;input, T &amp;output) {
475 565 }
476 566  
477 567 /// Non-string parsable by a stream
478   -template <typename T,
479   - enable_if_t<!std::is_floating_point<T>::value && !std::is_integral<T>::value &&
480   - !std::is_assignable<T &, std::string>::value &&
481   - !std::is_constructible<T, std::string>::value && !std::is_enum<T>::value &&
482   - !is_direct_constructible<T, double>::value && !is_direct_constructible<T, int>::value,
483   - detail::enabler> = detail::dummy>
  568 +template <typename T, enable_if_t<classify_object<T>::value == other, detail::enabler> = detail::dummy>
484 569 bool lexical_cast(const std::string &input, T &output) {
485 570 static_assert(is_istreamable<T>::value,
486   - "option object type must have a lexical cast overload or streaming input operator(>>) defined if it "
  571 + "option object type must have a lexical cast overload or streaming input operator(>>) defined, if it "
487 572 "is convertible from another type use the add_option<T, XC>(...) with XC being the known type");
488 573 return from_stream(input, output);
489 574 }
... ...
tests/AppTest.cpp
... ... @@ -647,6 +647,15 @@ TEST_F(TApp, BoolOption) {
647 647 args = {"-b", "-7"};
648 648 run();
649 649 EXPECT_FALSE(bflag);
  650 +
  651 + // cause an out of bounds error internally
  652 + args = {"-b", "751615654161688126132138844896646748852"};
  653 + run();
  654 + EXPECT_TRUE(bflag);
  655 +
  656 + args = {"-b", "-751615654161688126132138844896646748852"};
  657 + run();
  658 + EXPECT_FALSE(bflag);
650 659 }
651 660  
652 661 TEST_F(TApp, ShortOpts) {
... ...
tests/OptionGroupTest.cpp
... ... @@ -622,7 +622,7 @@ TEST_F(ManyGroups, Moving) {
622 622 }
623 623  
624 624 struct ManyGroupsPreTrigger : public ManyGroups {
625   - size_t triggerMain, trigger1{87u}, trigger2{34u}, trigger3{27u};
  625 + size_t triggerMain{0u}, trigger1{87u}, trigger2{34u}, trigger3{27u};
626 626 ManyGroupsPreTrigger() {
627 627 remove_required();
628 628 app.preparse_callback([this](size_t count) { triggerMain = count; });
... ...