1

I am trying to simplify a C/C++ codebase that invokes SQL Server DB statements and/or stored procedures from SQL Server by writing variadic templates of various sorts. I have been successful writing a doQuery function that accepts additional parameters that are automatically bound to the query by type, e.g. boolean, double, string, as input or input/output arguments.

The current problem:

For queries that return result sets, I am struggling to produce optimal simple template calls.

I have tried tuples, but they are unpopular among a traditional C developer team, and I don't like them myself.

So I accept a function that is called back with the data of each row which the user can choose to push/init{} in any form, e.g. push into a captured vector of structs. From the signature of the callback, I generate the SQLBindCol bindings for ODBC to nth degree of complexity into an array of binding information for the returning columns.

e.g.

std::vector<MyStruct> result;

auto functor = [&result](int id, string name, bool isOK, string address) {
  result.push_back(MyStruct{id,name,isOK,address});
}

c.doQuery(functor, "select id, name, isOK, address from MyTable;" /**/);

(/**/ shows where parameters can be supplied to fill in '?', which I have working beautifully, but this simple example has none).

The problem comes trying to invoke the function reassociating query row results:

template <typename T...>
InternalFunction(T)

[...]

vector<Binding> columns;

[...]

int i = 0;
functor(GetResult<T>(columns, i++)...);

GetResult returns the column from the ith member of the binding vector as the desired type into the function call. But C++ does not guarantee order of evaluation of arguments. So according to https://stackoverflow.com/users/596781/kerrek-sb in the thread How to guarantee order of argument evaluation when calling a function object?, the solution is fairly simple:

struct OrderedCall
{
    template <typename F, typename ...Args>
    OrderedCall(F && f, Args &&... args)
    {
        std::forward<F>(f)(std::forward<Args>(args)...);
    }
};

[...]

OrderedCall{functor, GetResult<T>(columns, i++)...};

Unfortunately, Visual Studio 15 (MSVC) is apparently broken and still reorders the evaluations, e.g. if there is a string argument, that gets evaluated last, making the results come from arbitrarily selected members of the column vector instead of picking them up in order as they were auto-bound using the variadic methods.

I do not know of any other way to associate the arguments with successive vector entries to solve this problem and make the functor callback work reliably.

If I disable optimization and function inlining and remove the OrderedCall struct trick, the evaluations will occur fairly reliably in reverse order which is usable, but is not a good solution because it is ultimately neither practical nor reliable.

I cannot think of another way to invoke the function while: re-associating arguments with columns, preserving types of the arguments, etc.

The whole point is to make the binding simple and natural, which is why tuples are ruled out together with other solutions that uglify the calling pattern.

So my questions are:

  • Is there some way to invoke my std::function with the set of args generated by templated function from the vector? I looked at std::bind, but that didn't appear to offer a simple way to solve the problem, but maybe iterative binding of one arg at a time, deriving one function from another from another...

  • Should I engineer a recursive template just for the purposes of invoking the functor? (it would be an ugly implementation at best passing arguments through many layers of complexity, if it works at all).

  • Did I miss a simpler/better solution for making it dead simple to push result rows into a vector of structs?

  • Am I re-inventing the wheel? Is this variadic binding simplification already available for sql-server / odbc / msvc?

  • Is a later version of Visual Studio not broken like this?

I answered the VS brokenness myself with the following test:

#include <iostream>
#include <string>

struct OrderedCall
{
        template <typename F, typename ...Args>
        OrderedCall(F && f, Args &&... args)
        {
                std::forward<F>(f)(std::forward<Args>(args)...);
        }
};
void foo(int, std::string s, char, bool) {
        int i = 0;
        i++;
}

int bar() {
        return 10;
}

template <typename T>
T echo(T x) {
        std::cout << " " << x;
        return x;
}

int main(int argc, char**argv) {
        std::string goo = "Goo";

        std::cout << "Occurance: " << bar() << " " << goo << " " << 'x' << " " << false;

        std::cout << std::endl << "Unordered call:";
        foo(echo(bar()), echo(goo), echo('x'), echo(false));

        std::cout << std::endl << "Ordered call:";
        OrderedCall{ foo, echo(bar()), echo(goo), echo('x'), echo(false) };

        std::cout << std::endl << "Try in Debug and Release modes. " << std::endl
                << "Ordered should match Occurance" << std::endl;
}

Apparently still broken in lastest update of Visual Studio 19, the following in Debug mode.

Occurance: 10 Goo x 0
Unordered call: Goo 0 x 10
Ordered call: 0 x 10 Goo
Try in Debug and Release modes.
Ordered should match Occurance

See: https://timsong-cpp.github.io/cppwp/n3337/dcl.init.list and https://timsong-cpp.github.io/cppwp/n3337/class.expl.init

  1. Within the initializer-list of a braced-init-list, the initializer-clauses, including any that result from pack expansions ([temp.variadic]), are evaluated in the order in which they appear. That is, every value computation and side effect associated with a given initializer-clause is sequenced before every value computation and side effect associated with any initializer-clause that follows it in the comma-separated list of the initializer-list. [ Note: This evaluation ordering holds regardless of the semantics of the initialization; for example, it applies when the elements of the initializer-list are interpreted as arguments of a constructor call, even though ordinarily there are no sequencing constraints on the arguments of a call. — end note ]

[The above note is in the text, not added by me.]

Xam
  • 11
  • 3
  • You might be looking for [`make_integer_sequence`](https://en.cppreference.com/w/cpp/utility/integer_sequence). The example on that page shows how to use it to expand parameter packs. – Igor Tandetnik Apr 15 '20 at 15:28
  • I did not see in make_integer_sequence how to expand parameter pack into function call. So I made a recursive lambda implementation that builds a chain of lambdas one argument per recursion without make integer sequence. The needless recursion still breaks argument types, e.g. unique_ptr, which breaks on std::forward, which the flat implementation had no trouble with. I will log a bug to Microsoft for making the obtuse recursive implementation necessary. This shouldn't be lisp and I have a hard time believing it doesn't hurt performance in addition to functionality. I can post code. – Xam Apr 16 '20 at 00:25
  • A typical approach is, you add a helper function like `template void helper(std::integer_sequence) { functor(GetResult(columns, Is)...); }`. Then call it as `helper(std::make_integer_sequence());` where `N` is the number of elements you want the pack to expand to. `Is` will be deduced as the sequence `0, 1, ..., N-1` – Igor Tandetnik Apr 16 '20 at 03:08
  • An integer function parameter can be incremented while recursively constructing lambdas, captured with binding vector, status tracking, any other details. Sad to have to go to the trouble. It is easy to notice side effects and only re-order where optimising without visible side effects. The standard requires it. I've watched output of quality compilers that get a standard right after 10 years. Before optimisation lambda recursion with function casts at each level for template selection is a mess. Does MSVC optimise that all away? Better to fix the compiler and avoid template recursion. – Xam Apr 16 '20 at 13:47
  • `integer_sequence` does not rely on recursion, or side effects. The resulting code will be equivalent to `functor(GetResult(columns, 0), GetResult(columns, 1), ..., GetResult(columns, N-1));` Is this not what you want? – Igor Tandetnik Apr 16 '20 at 13:59
  • I will have to study it. A flat expansion, it is exactly what I want. – Xam Apr 16 '20 at 18:05
  • I cannot find an example that conflates a parameter pack of argument types with successive integers as required in this case. Still lookng. – Xam Apr 17 '20 at 12:57
  • I'm not quite sure what you mean by "conflates". Are you perhaps looking for something like [this](https://rextester.com/PRGZ62530)? – Igor Tandetnik Apr 17 '20 at 13:40
  • What makes you believe Visual Studio is "broken"? In C++, the order of evaluation of function arguments is unspecified; this is what your example observes. This includes arguments passed to a constructor, so it's unclear why you expect `OrderedCall` to behave differently. – Igor Tandetnik Apr 17 '20 at 14:43
  • https://timsong-cpp.github.io/cppwp/n3337/dcl.init.list This is a description of how the braced-init list works. It is clear from the content that this is covering the mapping of the braced-init list to constructor arguments which is how OrderedCall works. Look specifically at section 4. I think it leaves no room for doubt that my simple example is misinterpreted by MSVC. – Xam Apr 21 '20 at 10:14
  • Your additional example looks promising. If it works as I try to apply it to the problem at hand, it is a welcome gift. THANKS in advance. I am not surprised it has to go beyond the simple examples in the other references. I am a bit of a novice when it comes to variadic pack expansion, so a function with double pack expansion makes me less sure that I can expect it to work, and I wonder, e.g., what the in-tandem expansion does if the pack sizes do not match. – Xam Apr 21 '20 at 10:49
  • If the packs are of different size, the program is ill-formed and the compiler complains. – Igor Tandetnik Apr 21 '20 at 10:54
  • It would have been nice. Visual Studio 2019 rejects the clang solution: |Error C3520 'Args': parameter pack must be expanded in this context Test Test.cpp 13 |Error C3520 'args': parameter pack must be expanded in this context Test Test.cpp 13 |Error C3520 'Is': parameter pack must be expanded in this context Test Test.cpp 13 |Error C2672 'PrintWithIndex': no matching overloaded function found Test.cpp 13 |Error C2059 syntax error: '...' Test Test.cpp 13 – Xam Apr 23 '20 at 19:43
  • I am going to have to see if I can file multiple bugs against Visual Studio. I wish I had the option of using clang. Even the stale old default gnu c++ compiler on my workhorse linux box compiles and correctly executes both your conflated std::integer_sequence example and the OrderedCall example. Which one is created by hobbyists? – Xam Apr 23 '20 at 20:15
  • 0 Im having the same issue and will probably simply write non variadic overloads taking up to maybe 10 template arguments. Obviously that is brain dead copy paste code I can get rid of as soon as the issue is fixed. Keep upvoting it ;-) – Marti Nito Jun 28 '20 at 20:53

0 Answers0