Motivation
Although I love coding in C++, I sometimes yearn for the syntactic sugar of Python. C++11 has somewhat eased the pain by such beautiful analogies like this:
# python
for i in [1, 5, 7]:
// C++11
for(auto i: {1, 5, 7})
Unfortunately, C++ puts an end to this as soon as you want to do such crazy things as having the element index in a range based for.
Where Python gives you
for index, element in enumerate(elements):
C++ leaves us with writing a "normal" loop like
for(int index = 0; index < elements.size(); ++i)
//do something with elements[index]
(which is slow with non random access containers) or with uglier loops that involve iterators. Fortunately, it is possible to translate the beauty of Python and write a range adaptor that returns an (index, element&) tuple (or pair) and could be created and used like this:
for(auto indexElementPair: enumerate(elements))
Yet, this is only half the beauty of Python since we have to deal with this ugly pair using it like indexElementPair.first which is not the nicest form.
Usage
This is where my code comes into play. I wanted to emulate the behavior of Python as close as possible. Although it is not possible to declare multiple (auto) variables in the range based for header (and unpack the tuple into them) we can do so right after it:
for(auto indexElementPair: enumerate(elements)) {
DECLARE_AND_TIE((index, element), indexElementPair);
// use index and element like in the python code
}
Note that the index and element variables are both declared as references and their type is automatically deduced from the corresponding tuple element.
The code is on github and in the following files:
declare_and_tie.hpp
#ifndef _guard_DECLARE_AND_TIE_HPP_
#define _guard_DECLARE_AND_TIE_HPP_
#if not(BOOST_PP_VARIADICS)
#error "Boost preprocessor variadics support needed for DECLARE_AND_TIE!"
#endif
#include <boost/preprocessor/tuple/size.hpp>
#include <boost/preprocessor/tuple/to_seq.hpp>
#include <boost/preprocessor/seq/for_each_i.hpp>
#include <boost/preprocessor/if.hpp>
#include <boost/preprocessor/variadic/size.hpp>
#include <boost/preprocessor/comparison/equal.hpp>
#include <boost/preprocessor/expand.hpp>
#include <boost/mpl/aux_/preprocessor/is_seq.hpp>
#include "is_boost_pp_tuple.hpp"
#include "require_trailing_semicolon.hpp"
/**
* @brief Declare references to tuple elements with the given names
* @param UNPACKED_ELEMENTS a boost preprocessor tuple of names for references
* @param TUPLE to unpack
*
* Example usages:
*
* std::tuple<int, float, string> cppTuple;
* DECLARE_AND_TIE((a, b, c), cppTuple);
*
* std::tuple<int, float, string> cppTuple;
* //ignoring the float element
* DECLARE_AND_TIE((a, _, c), cppTuple);
*/
#define DECLARE_AND_TIE(UNPACKED_ELEMENTS, TUPLE) \
static_assert(BOOST_PP_IS_TUPLE(UNPACKED_ELEMENTS), \
"First parameter must be a tuple!"); \
static_assert( \
!COMPARE_PP_TUPLE_TO_CPP_TUPLE_SIZE(UNPACKED_ELEMENTS, >, TUPLE), \
"Too many unpacked elements for tuple"); \
static_assert( \
!COMPARE_PP_TUPLE_TO_CPP_TUPLE_SIZE(UNPACKED_ELEMENTS, <, TUPLE), \
"Too few unpacked elements for tuple"); \
_impl_DECLARE_AND_TIE_VARIABLES(UNPACKED_ELEMENTS, TUPLE); \
REQUIRE_TRAILING_SEMICOLON()
#define COMPARE_PP_TUPLE_TO_CPP_TUPLE_SIZE(UNPACKED, REL, TUPLE) \
(BOOST_PP_TUPLE_SIZE(UNPACKED) REL std::tuple_size<decltype(TUPLE)>::value)
#define _impl_DECLARE_AND_TIE_ONE_VARIABLE(r, TUPLE, INDEX, VARNAME) \
BOOST_PP_IF(_impl_TOKEN_IS_UNDERSCORE(VARNAME), \
_impl_DECLARE_AND_TIE_UNDERSCORE, \
_impl_DECLARE_AND_TIE_VARIABLE)(r, TUPLE, INDEX, VARNAME)
#define _impl_TOKEN_IS_UNDERSCORE_TEST_(x) x
#define _impl_TOKEN_IS_UNDERSCORE(a) \
BOOST_MPL_PP_IS_SEQ( \
BOOST_PP_CAT(_impl_TOKEN_IS_UNDERSCORE_TEST, a)((unused)))
#define _impl_DECLARE_AND_TIE_UNDERSCORE(r, TUPLE, INDEX, VARNAME)
#define _impl_DECLARE_AND_TIE_VARIABLE(r, TUPLE, ELEMENT_INDEX, VARIABLE_NAME) \
auto& VARIABLE_NAME = std::get<ELEMENT_INDEX>(TUPLE);
#define _impl_DECLARE_AND_TIE_VARIABLES(UNPACKED_ELEMENTS, TUPLE) \
BOOST_PP_SEQ_FOR_EACH_I(_impl_DECLARE_AND_TIE_ONE_VARIABLE, TUPLE, \
BOOST_PP_TUPLE_TO_SEQ(UNPACKED_ELEMENTS))
#endif /* _guard_DECLARE_AND_TIE_HPP_ */
require_trailing_semicolon.hpp
#ifndef REQUIRE_TRAILING_SEMICOLON_HPP_
#define REQUIRE_TRAILING_SEMICOLON_HPP_
#include <boost/preprocessor/cat.hpp>
#define REQUIRE_TRAILING_SEMICOLON() \
struct BOOST_PP_CAT(trailing_semicolon_required_on_line_, __LINE__) {}
#endif /* REQUIRE_TRAILING_SEMICOLON_HPP_ */
unit_tests.cpp
#include "test.hpp"
#include <declare_and_tie.hpp>
BOOST_AUTO_TEST_CASE(Test_reference_to_tuple_elements) {
auto tuple = std::make_tuple(1, 0.5, std::string("hello world"));
DECLARE_AND_TIE((a, b, c), tuple);
BOOST_REQUIRE(std::is_reference<decltype(a)>::value);
BOOST_REQUIRE_EQUAL(&a, &std::get<0>(tuple));
BOOST_REQUIRE(std::is_reference<decltype(b)>::value);
BOOST_REQUIRE_EQUAL(&b, &std::get<1>(tuple));
BOOST_REQUIRE(std::is_reference<decltype(c)>::value);
BOOST_REQUIRE_EQUAL(&c, &std::get<2>(tuple));
}
template <class T>
using isConstRef = std::is_const<typename std::remove_reference<T>::type>;
BOOST_AUTO_TEST_CASE(Test_retain_tuple_constness) {
auto const tuple = std::make_tuple(1, 0.5, std::string("hello world"));
BOOST_REQUIRE(std::is_const<decltype(tuple)>::value);
DECLARE_AND_TIE((a, b, c), tuple);
BOOST_REQUIRE(isConstRef<decltype(a)>::value);
BOOST_REQUIRE(isConstRef<decltype(b)>::value);
BOOST_REQUIRE(isConstRef<decltype(c)>::value);
}
BOOST_AUTO_TEST_CASE(Test_retain_tuple_element_constness) {
std::tuple<const int, double, const char*> tuple(1, 0.5, "hello world");
BOOST_REQUIRE(!std::is_const<decltype(tuple)>::value);
DECLARE_AND_TIE((a, b, c), tuple);
BOOST_REQUIRE(isConstRef<decltype(a)>::value);
BOOST_REQUIRE(!isConstRef<decltype(b)>::value);
// the pointer is not const!
BOOST_REQUIRE(!isConstRef<decltype(c)>::value);
}
BOOST_AUTO_TEST_CASE(Test_multiple_ties) {
const auto tuple = std::make_tuple(1, 0.5, std::string("hello world"));
DECLARE_AND_TIE((a, b, c), tuple);
DECLARE_AND_TIE((d, e, f), tuple);
}
/*
BOOST_AUTO_TEST_CASE(Test_error_on_rvalue_cpp_tuple) {
DECLARE_AND_TIE((integer, real, string),
std::make_tuple(1, 0.5, std::string("hello world")));
}
*/
/*
BOOST_AUTO_TEST_CASE(Test_error_on_non_tuple_unpack_list) {
auto tuple = std::make_tuple(1, 0.5, std::string("hello world"));
DECLARE_AND_TIE(one_parameter, tuple);
}
*/
/*
BOOST_AUTO_TEST_CASE(Test_error_on_missing_trailing_semicolon) {
auto tuple = std::make_tuple(1, 0.5, std::string("hello world"));
// error about missing semicolon
DECLARE_AND_TIE((a, b, c), tuple)
}
*/
/*
BOOST_AUTO_TEST_CASE(Test_error_on_too_many_unpack_elements) {
std::tuple<const int, double, std::string> tuple(1, 0.5, "hello world");
DECLARE_AND_TIE((a, b, c, d), tuple);
}
*/
/*
BOOST_AUTO_TEST_CASE(Test_error_on_too_few_unpack_elements) {
std::tuple<const int, double, std::string> tuple(1, 0.5, "hello world");
DECLARE_AND_TIE((a, b), tuple);
}
*/
I omitted the code for is_boost_pp_tuple.hpp because it is not mine and it is only included because I could not find it in the boost sources (although there is a commit for it on some non existing SVN branch).
Obviously many of the unit tests are not compilable. Instead they should poster-child common usage errors and the expected error messages. If you have any idea of how to turn these dead tests into living ones I am very keen to hear about them.
Features
- automatic type deduction (includes constness)
- sane error messages on wrong usage (at least on gcc):
- too many/few unpack elements
- first argument not a Boost.Preprocessor tuple
- second argument not a C++ tuple
- second argument is a temporary (or rather rvalue)
- no runtime cost for ignoring elements via
_(use likeDECLARE_AND_TIE((a, _, _), tuple)to ignore all but the first element)
Review goals
- correctness (and viability) of the approach including
- dangling reference/pointer problems
- exception safety
- concurrency problems
- (all) features supported on your platform of choice (I cannot test all compilers and operating systems, note that I require C++11 compliancy)
- usability issues (especially unanticipated error case that give unhelpful error messages)
- readability (and naming)
- performance issues
In summary I want to know of any better way to achieve this.
I would also like to hear your opinion whether the _ placeholders are sensible. They complicate the design of DECLARE_AND_TIE and introduce a dependency on BOOST_MPL_PP_IS_SEQ from boost/mpl/aux_/preprocessor/is_seq.hpp which does not look like a stable include path.
In all use cases that I could think of they (the _ placeholders) could have been prevented by not having the ignored elements in the tuple in the first place.
Known "error"
One situation where there is no sensible error message is the case of providing an empty preprocessor tuple for a one element C++ tuple:
std::tuple<int> tuple;
DECLARE_AND_TIE((), tuple);
This will dodge the size test because an empty preprocessor tuple has the size 1. There will be an error message because the variable that gets declared has no name.
I don't think it is very important to correct this because using a one element tuple is very unlikely in the first place and giving an empty unpack list should be obviously wrong. However, if you can show me that this problem is bigger than I thought or have a very easy workaround I would like to know.
The same problem (empty PP tuples having size 1) leads to this code giving a spurious error:
DECLARE_AND_TIE((), empty_tuple);
The size check will determine that the PP tuple has size 1 whereas empty_tuplehas size 0. However, I don't think that empty tuples should be a use case (but again I would like to hear if I am wrong on this).
More motivation
Some of the arguments are along the line that this too much overhead for two lines of reference declarations. My initial motivating example is only a small subset. The technique should be applicable to macros of any size. Take for example this hypothetical code:
for(auto tuple: zip(numbers, names, ages, addresses)) {
DECLARE_AND_TIE((number, name, age, address), tuple);
// ...
}
You might find it acceptable to write instead
for(auto tuple: zip(numbers, names, ages, addresses)) {
auto &number = std::get<0>(tuple);
auto &name = std::get<1>(tuple);
auto &age = std::get<2>(tuple);
auto &address = std::get<3>(tuple);
}
This is a lot of boilerplate code that allows for errors like wrong type declarations and incorrect indices (which is very likely due to copy&paste coding).
DECLARE_AND_TIE((index, element), indexElementPair);overauto& index = indexElementPair.first; auto& element = indexElementPair.second;is outweighed by its complexity. It seems like something I would be trying to convince myself I could accept after spending hours on it, rather than something I should actually use. \$\endgroup\$